依赖收集就是订阅数据变化watcher的收集,依赖收集的目的是当响应式数据发生变化时,能够通知相应的订阅者去处理相关的逻辑。
在上一章,介绍了Vue将普通对象变成响应式对象是利用defineReactive()(定义在'core/observer/index.js'中)函数,defineReactive()函数中主要关注的是新建一个dep = new Dep(),以及在设置属性的getter时,对其做依赖收集:dep.depend()
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } 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 /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
定义在'core/obersver/dep.js'中的Dep类,主要功能是建立了observer与watcher之间的桥梁。depend()函数的作用是在调用Dep.target的addDep()函数。
这里的Dep.target是一个静态属性,在全局中唯一保存着一个Watcher,因为在同一时间应该只有一个Watcher被计算。
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
在watcher.js中定义了Watcher类,其中的deps和newDeps数组中保存了用来保存watcher的依赖类。
其中的addDep()函数主要是先通过id判断在新收集的依赖中是否有相同的依赖,没有的话,将其保存在newDeps数组中,然后再判断在以前保存的依赖中是否有相同的,没有则调用dep的addSub函数,将依赖的Watcher收集起来。这个过程主要是为了去重。
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; computed: boolean; sync: boolean; dirty: boolean; active: boolean; dep: Dep; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } /** * Evaluate the getter, and re-collect dependencies. */ 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 { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } /** * Add a dependency to this directive. */ 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) } } }
以渲染watcher为例说明一下过程。在执行$mount函数的过程中,最后会调用mountComponent函数,其中有一段这样的代码。
在实例化Watcher是,将updateComponent函数传入作为getter。
let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') }
在watcher中,会调用get函数
} else { this.value = this.get() }
可以看到,在get函数中,首先会pushTarget(),这个函数的功能是在将当前执行的Dep.target保存起来。以便在组件嵌套时能够恢复上一轮运行的Dep.target。
然后调用this.getter.call(vm,vm),这里的getter就是updateComponent()函数,于是它会执行render函数。render函数在执行过程中会存在访问数据的情况,于是就触发getter。
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 { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target }
在每个对象的属性中都持有一个dep,在触发getter之后,就会调用dep.depend( )函数,然后调用Dep.target.addDep( )函数,该函数中经过去重判断之后,调用dep.addSub将渲染watcher保存在subs数组中。
回到watcher中的get( )函数,在执行完getter之后,还会执行traverse( )函数,这是为了将value中存在的属性递归执行。
if (this.deep) { traverse(value) }
以及popTarget( )函数,该函数的作用是恢复上一轮执行的Dep.target。
popTarget()
然后执行cleanupDeps( ) 函数。其功能是判断在新收集的依赖中是否出现在之前的依赖收集中,此前在addDep( ) 函数中也判断过一次,但那个判断的目的是为了在deps数组和newDeps数组中去重。这次判断是为了丢掉不需要再监听的属性,避免重复触发渲染。然后再将
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 }
在触发数据的getter之后,会收集当前运行的watcher保存在dep的subs数组中。同时,在watcher类中,保存了相关的deps和newDeps,用来到达去重的同时,还可以清除不再需要观察的依赖。