赞
踩
Vue是当前最流行的框架之一,现在很多项目都或多或少都会用到Vue。所以了解Vue的响应式原理对我们意义非凡,有利于…
我们直接开始吧
Vue对数据进行响应式的处理的入口在src/core/instance/state.js
文件下的initState
函数
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
函数参数vm指的是Vue实例,我们可以把它看作是我们平常写vue代码是经常使用的this
。
函数首先给vm实例定义_watchers
属性,这个什么作用我们暂时不用管,接着往下。
然后获取vm的
o
p
t
i
o
n
s
配
置
。
这
里
的
options配置。这里的
options配置。这里的options配置包括我们Vue预先定义的配置、mixins传入的配置,和我们自己实例话Vue时传入的配置。
比如我们经常这样实例化Vue
new Vue({
data() {
return {
a: 1
}
}
})
通过$options就能拿到我们传入的配置。
后面的代码我们很容易就能理解了,Vue拿到我们传入的props、methods、data、computed、watch属性分别进行初始化。我们在这里主要关注data的初始化,所以接下来我们看看initData做了什么
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */) }
initData中首先拿到data的数据,并将数据赋值为vm._data, 如果时data是个函数的话,就调用getData获取数据
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
然后判断data是否是个对象,接着判断data中属性的名字是否和props和methods中属性命名冲突,是否时Vue的保留字(_isReserve
)
如果data中的数据的属性不和props和method命名冲突,也不是以$
和_
开头(Vue自己定义的属性以$
和_
开头),那么久调用proxy
函数对data中数据进行代理
proxy函数
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }
proxy函数非常简单,主要通过 Object.defineProperty 函数在实例对象 vm 上定义与 data 数据字段同名的访问器属性,并且这些属性代理的值是 vm._data
上对应属性的值,所以我们平常使用的this.a
其实是this._data.a
中的值
接下来调用observe函数对data中的数据进行响应式处理,所以说,从这里开始才是Vue响应式系统的开始,接下来我们开看看observe函数做了什么
observe定义在src/core/observer/index.js文件中
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
//...
ob = new Observer(value)
//..
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
为了方便理解,代码删除了一些条件判断。从上面的代码很容易就可以看出,observe函数首先判断传入的value是不是对象,如果是对象才进行响应式处理,毫无疑问,我们传入的data是个对象,接着定义了一个Oberser对象并返回,接下来我们看看Observer
是怎样的
Observer定义在通过一个目录下,是一个类(构造函数)
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } // ... }
在connstructor方法中,首先将传入的value(也就是data)赋值为this.value,实例对象的 dep 属性,保存了一个新创建的 Dep实例对象。然后初始化vmCount,之后给我们的data定义一个__ob__
属性,指向创建的实例。 也就是说,如果我们传入的data是
data = {
a: 1
}
这样的话,经过def(value, '__ob__', this)
就变成了
data = {
a: 1
__ob__: {
vmCount: 0,
dep: Dep实例,
...
}
}
接下来就进入了分支判断,判断value是否是数据,因为Vue对普通对象和数组的响应式策略不同,所以需要进行判断,在这里我们主要关注Vue对普通对象的响应式处理
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
walk函数非常简单,遍历obj的每一个属性,然后调用defineReactive
函数
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 为每个属性定义一个dep实例,作为依赖收集器 const dep = new Dep() // 判断属性是否可以被配置 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 获取getter和setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果val是对象,先递归对其属性进行响应式处理 let childOb = !shallow && observe(val) // 对自己响应式处理 Object.defineProperty(obj, key, { enumerable: true, configurable: true, // 拦截获取值的操作,并收集依赖 get: function reactiveGetter () { // 获取值 const value = getter ? getter.call(obj) : val // 收集依赖 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, // 拦截赋值操作,并通知以来数据更新 set: function reactiveSetter (newVal) { // 获取原来的值 const value = getter ? getter.call(obj) : val // 判断是否需要更新 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 如果属性不可修改,直接返回 if (getter && !setter) return // 修改数据 if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新的值进行响应式处处理 childOb = !shallow && observe(newVal) // 通知所有的依赖,数据进行了更新 dep.notify() } }) }
defineReactive
函数有点复杂,不过主要值有三点
this.a = xxx
对对数据进行更新,从而使得视图得到更新通过前面的讨论,我们已经知道了Vue会在获取data的值的会调用getter,进行执行dep.depend()
时候进行依赖收集,那么依赖收集又是什么实现的呢?而且dep
具体又是什么,接下来我们就讨论一下这部分内容
我们下来了解一下数据响应系统中另一个很重要的部分——Watcher
。其实Watcher
就是我们前面所说的依赖,dep
收集依赖就是收集watcher
,当data更新是,会通知它所收集的watcher数据得到了更新,进而watcher就会执行相应的逻辑,更新视图。
每一个Vue实例都对应一个Watcher
,所以每次data数据得到更新时,Vue实例对应的Watcher
都会得到通知,进而触发视图更新
那么Vue实例对应的Watcher时什么时候创建的呢?
wacher的创建在Vue挂在模板时进行创建。我们可以在src/core/instance/lifecycly.js中的mountComponent函数中看到Watcher的创建
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { //... let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // ... const vnode = vm._render() // ... vm._update(vnode, hydrating) // ... } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) ... }
在面代码中,定义了updateComponent函数,从名字可以看出,这个是用来更新视图的,开始创建和数据更新时都会调用这个函数来更新视图。updateComponent中调用了vm._render
函数,这个函数的作用就是将我们的返回一个VNode
(虚拟DOM),也就是在这个函数中我们得到data的值,触发getter,进行依赖收集。我们可以通过一个例子来看一下,如果我们自己定义render函数的话:
new Vue({
data() {
return {
a: 1
}
},
render: function(createElemnet) {
let a = this.a
return createElement('div', a);
}
})
vm._render
会调用到这里的render
函数,在render函数中,我们获取a的值,触发了a的getter,随之进行依赖收集
接下来我们看看Watcher的定义,在core/observer/watcher.js下。
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() // ... if (typeof expOrFn === 'function') { this.getter = expOrFn } this.value = this.lazy ? undefined : this.get() // .. } // ... }
在构造函数中定义了一系列属性。因为options没有传入deep、user、lasy、sync
,所以this.deep
等的值都为false
,this.deps
和this.depIds
时用来保存收集器的,也就是该watcher
被哪些收集器所收集,而newDepIds
和newDeps
是为了解决重复依赖的问题的。将我们传入的expOrFn(这里是updateComponent函数)赋值给gettter(这里很重要)。最后调用get函数。
下面我们就看看get函数做了什么
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
该函数首先调用了pushTarget函数,最后调用了popTarget,这两个函数就定义在core/observer/dep.js下
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
这里就很关键了,就是在这里Vue将Dep.target
设为当前的Watcher
,因为Vue中会存在多个Watcher(computed属性会对应一个Watcher
,我们自定义的watch也会对应Watcher
),所以在pushTarget函数中会先将和原来的Dep.target
放入targetStack
,在popTarget
函数再将Dep.Target
设为原来的Dep.Target
。
之后get函数就执行
value = this.getter.call(vm, vm)
就是刚刚定义updateComponent
函数,updateComponent
调用vm._render
函数,在vm._render
函数中,我们获取data中的数据,触发其getter
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 收集依赖
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
这时Dep.target
不为空,所以就进入dep.depend
,然后就可以收集依赖了
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
depend函数很简单,调用了Watcher的addDep函数
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addDep函数首先获取dep的id,然后判断依赖是否被收集了,如果没有这里的判断,下面的情况会出现重复收集依赖。
render: function(createElement) {
let a1 = this.a
let a2 = this.a
}
然后就将当前Watcher,加入到dep的依赖收集器中
addSub (sub: Watcher) {
this.subs.push(sub)
}
我们回到Watcher的get函数中,在执行了popState函数后,然后执行this.cleanupDeps()
函数, 我们来看看这个函数的作用
cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
函数一开始会将dep中不依赖当前Watcher从sub中剔除, 然后就是简单的交换。将deps和depIds设置为最新的newDeps和newDepIds
依赖收集就到这里了,依赖收集了解之后,依赖更新就很容易理解了
我们使用this.a = Xxx后,会触发setter,然后触发依赖进行更新,触发依赖调用了Dep
的notify
,我们看看这个函数做了什么,函数定义在core/observer/dep.js下
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
notify的功能很简单,就是遍历所有的watcher,然后调用它们的update函数进行更新进行更新
至此,Vue响应式的原理就看完了,Vue源码博大精深,还有更广阔的天地等着大家探索,少年们加油吧
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。