最近面试的时候遇到了一个碰到了一个题目,大意如下,Vue中是如何对一个computed做到依赖收集的?当时没能回答上来,面试结束后做了一个小小的梳理。在这里做一个总结
题目大意
题目大意如下,在Vue.js中如何对this.a做到依赖收集与更新派发的
1 | computed: { |
我们都知道在Vue.js中对data对象数据的依赖收集在Object.defineProperty的get方法中,具体实现大概如下
1 | Object.defineProperty(obj, key, { |
Computed 的依赖收集
那Computed 是如何做到依赖收集的呢?还是看源码
1 | const computedWatcherOptions = { lazy: true } |
可以看到在Vue中,每一个 computed ,都是一个Watcher ,这就意味着它可以像组件Watcher一样,去做一些依赖收集的工作,在这里着重提一点const computedWatcherOptions = { lazy: true },可以看到这个配置在创建Watcher时被导入,这个配置也是computed与watch的最大区别。
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
Watcher的代码实现
1 | export default class Watcher { |
可以看到 实现的末尾 this.value = this.lazy? undefined: this.get(),因为此时的option中lazy:true实际上值undefined
回到问题
好了说了那么多,我们现在来回到问题。
一个简单的Demo
1 | <body> |
我们知道当我们去初始化组件的时候,此时的Dep.target指向该组件所在的Watcher,我在这就称它为Wapp,在渲染该组件时,会调用this.a,那么就会调用this.a的get函数
1 | export function defineComputed ( |
记住,此时的Dep.target依旧指向Wapp,调用get时,dirty初始值为true,调用watcher.evaluate()
1 | evaluate () { |
最终会执行watcher.get,此时pushTarget(this),Dep.target指向Computed,我们称他为CW,然后执行value = this.getter.call(vm, vm),即例子中的return this.message;,调用了message的get函数,那么message就会收集到CW的依赖。最后popTarget,此时Dep.target又变为WA
此时,整个evaluate执行结束
1 | if (watcher.dirty) { |
从上文我们可以分析到,在执行计算Watcher的get函数时,已经收集到了一些依赖。(this.deps = [Dep(a),Dep(b)]),此时执行watcher.depend()
1 | class Watcher{ |
此时让计算Watcher的收集的Dep再次执行depend,注意此时的Dep.target指向全局Watcher,则Dep(a),Dep(b)收集到了收集到了全局Watcher的依赖。
至此,计算Watcher的依赖收集完成
计算Watcher 的派发更新
在进行完上面一系列操作之后,我们可以大概得到一个这样的依赖收集情况。
Dep(a):[计算Watcher,全局Watcher]
Dep(b):[计算Watcher,全局Watcher]
当我们的this.a或者this.b发生变化时。对应的Dep执执行notify,通知各个Watcher执行callback。
计算Watcher 执行 callback,使
this.dirty = true全局Watcher 执行 callback,使得浏览器重新渲染组件。因为
计算Watcher的dirty为true,会被正确更新。
至此,computed的依赖收集,派发更新分析结束