最近面试的时候遇到了一个碰到了一个题目,大意如下,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的依赖收集,派发更新分析结束