/** * A dep is an observable that can have multiple * directives subscribing to it. */ exportdefaultclassDep{ static target: ?Watcher; id: number; subs: Array<Watcher>;
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(); } } }
// the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null; const targetStack = [];
exportfunctionpushTarget(_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target); Dep.target = _target; }
exportfunctionpopTarget() { Dep.target = targetStack.pop(); } /** * Remove an item from an array */ functionremove(arr, item) { if (arr.length) { const index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1); } } }
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ exportdefaultclassWatcher{ 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;
/** * 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); } } }
/** * Clean up for dependency collection. */ 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; } // ... } /** * Parse simple path. * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ const bailRE = /[^\w.$]/; functionparsePath(path) { if (bailRE.test(path)) { return; } const segments = path.split("."); returnfunction (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return; obj = obj[segments[i]]; } return obj; }; }
那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。
考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。
因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费;
总结
综上,我们对 Vue 数据的依赖收集过程已经有了认识,并且对这其中的一些细节做了分析。收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现,下一节我们来详细分析一下派发更新的过程。