一直都在网上查阅关于computed源码分析的文章,但是看起来都挺费劲的。以下是我的一些学习笔记:
一、初始化时
1、初始Vue时会通过initState方法,在代码中进行opts.computed字段的判断,从而进行initComputed方法对compute进行初始化。
function Vue(){... 其他处理initState(this)...解析模板,生成DOM 插入页面
}function initState(vm) { var opts = vm.$options; if (opts.computed) { initComputed(vm, opts.computed); }.....
}
2、initComputed源码分析(Vue会为compute专门新建一个计算watcher即computerWatcher,它的 get
函数是要执行用户定义的求值函数)
function initComputed(vm, computed) { // 首先定义了一个空的对象,用来存放所有计算属性相关的 watchervar watchers = vm._computedWatchers = Object.create(null); for (var key in computed) { var userDef = computed[key]; // Vue代码实现时compute有两种定义方法,一种是直接定义function,一种是设置get和set// 判断是哪种,然后设置gettervar getter = typeof userDef === 'function' ? userDef: userDef.get; // 每个 computed 都创建一个 watcher// watcher 用来存储计算值,判断是否需要重新计算watchers[key] = new Watcher(vm, getter, { // lazy表示这个 watcher 需要缓存// 传入lazy作用是在watch中把计算结果缓存起来,而不是每次使用都要重新计算lazy: true }); // 判断是否有重名的属性if (! (key in vm)) {defineComputed(vm, key, userDef);}}
}
3、defineComputed分析(defineComputed,它决定了用户在读取 computer.getter这个计算属性的值后会发生什么)
// 部分实现
function defineComputed(target, key, userDef) { // 设置 set 为默认值,避免 computed 并没有设置 setvar set = function(){} // 如果用户设置了set,就使用用户的setif (userDef.set) set = userDef.set Object.defineProperty(target, key, { // 包装get 函数,主要用于判断计算缓存结果是否有效get:createComputedGetter(key), set:set});
}function createComputedGetter(key) { return function() { // 从组件实例上拿到computed-watchervar watcher = this._computedWatchers[key]; // 如果 computed 依赖的数据变化,dirty 会变成true,从而重新计算,然后更新缓存值 watcher.valueif (watcher.dirty) {// 这里会求值 调用 getwatcher.evaluate();} // 这里也是个关键if (Dep.target) {watcher.depend();} return watcher.value}
}
首先 dirty这个概念代表脏数据,说明这个数据需要重新调用用户传入的 compute.get函数来求值了。我们暂且不管更新时候的逻辑,第一次在模板中读取到compute返回值的时候它一定是 true,所以初始化就会经历一次求值。
evaluate () {// 调用 get 函数求值this.value = this.get()// 把 dirty 标记为 falsethis.dirty = false
}
这个函数其实很清晰,它先求值,然后把 dirty
置为 false。也就是说,下次没有特殊情况再读取到 compute返回值的时候,发现 dirty
是false了,是不是直接就返回 watcher.value
这个值就可以了,这其实就是计算属性缓存的概念。
二、Compute数据更新
这里假设一个示例来解释:
<div id="app"><span @click="change">{
{sum}}</span>
</div>
<script src="./vue2.6.js"></script>
<script>new Vue({el: "#app",data() {return {count: 1,}},methods: {change() {this.count = 2},},computed: {sum() {return this.count + 1},},})
</script>
Compute更新也就是count更新怎么触发了sum更新获取新的值。
从初始化可以知道,watcher.evaluate()这一步是关键。
evaluate () {// 调用 get 函数求值this.value = this.get()// 把 dirty 标记为 falsethis.dirty = false
}
首先在初始化时, 也就是这里进入 this.get(),首先要明确一点,在模板中读取 { { sum }} 变量的时候,全局的 Dep.target 应该是 渲染watcher。(我的理解是这里的渲染watcher就相当于是渲染{ {sum}}在页面上)
在Watcher的定义中是使用 栈 targetStack的结构来存储Dep.target。
初始化时此时的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ] 。
// Watcher部分源码
Watcher.prototype.get = function() { // 改变 Dep.targetpushTarget() // getter 就是 watcher 回调var value = this.getter.call(this.vm, this.vm); // 恢复前一个 watcherpopTarget() return value
};
Dep.target = null;
var targetStack = [];function pushTarget(_target) { // 把上一个 Dep.target 缓存起来,便于后面恢复if (Dep.target) {targetStack.push(Dep.target);}Dep.target = _target;
}function popTarget() {Dep.target = targetStack.pop();
}
首先刚进去就 pushTarget,也就是把 计算watcher 自身置为 Dep.target,等待收集依赖。执行完 pushTarget(this) 后,Dep.target 变更为 计算watcher。
此时的 Dep.target 是 计算watcher,targetStack 是 [ 渲染watcher,计算watcher ] 。
然后会执行到 value = this.getter.call(vm, vm),其实 getter
函数,就是用户传入的 sum
函数。
sum() {return this.count + 1
}
当执行这段函数时需要获取this.count,就相当于触发了count的watcher.get()
// 在闭包中,会保留对于 count 这个 key 所定义的 dep
const dep = new Dep()// 闭包中也会保留上一次 set 函数所设置的 val
let valObject.defineProperty(vm, 'count', {get: function reactiveGetter () {const value = val// Dep.target 此时就是计算watcherif (Dep.target) {// 收集依赖dep.depend()}return value},
})
就相当于count 的 dep 会收集 计算watcher 作为依赖。
那么可以看出,count 会收集 计算watcher 作为依赖,是通过dep.depend()收集的
// dep.depend()
depend () {if (Dep.target) {// 这里的this指的是count的depDep.target.addDep(this)}
}
而这时的 Dep.target指代的是计算watcher,相当于又绕回到 计算watcher 的 addDep 函数上去了
// watcher 的 addDep函数
addDep (dep: Dep) {// 这里做了一系列的去重操作 简化掉 ......// 这里会把 count 的 dep 也存在自身的 sum 的 deps 上this.deps.push(dep)// 又带着 watcher 自身作为参数// 回到 dep 的 addSub 函数了dep.addSub(this)
}
经过一个收集之后,最后的结果:
// sum的计算watcher就是
{deps: [ count的dep ],dirty: false, // 求值完了 所以是falsevalue: 2, // 1 + 1 = 2getter: ? sum(),lazy: true
}// count的dep
{subs: [ sum的计算watcher ]
}
可以看出,计算属性的 watcher 和它所依赖的响应式值的 dep,它们之间互相保留了彼此,相依为命。
当求值结束时,即 this.count+1计算结束时:
get () {pushTarget(this)let valueconst vm = this.vmtry {value = this.getter.call(vm, vm)} finally {// 此时执行到这里了popTarget()}return value
}
回到 计算watcher 的 getter 函数,执行到了 popTarget,计算watcher 出栈。
Dep.target 变更为 渲染watcher(即最初的watcher)
此时的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ]
然后函数执行完毕,返回了 2 这个 value,此时对于 sum 属性的 get 访问还没结束。
Object.defineProperty(vm, 'sum', { get() {// 此时函数执行到了这里if (Dep.target) {watcher.depend()}return watcher.value}}
})
此时的 Dep.target 就是 渲染watcher,所以进入了 watcher.depend() 的逻辑
// watcher.depend
depend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}
}
但是计算watcher ,它的 deps 里保存了 count 的 dep
也就是说,又会调用 count 上的 dep.depend()
class Dep {subs = []depend () {if (Dep.target) {Dep.target.addDep(this)}}
}
这次的 Dep.target 已经是 渲染watcher 了,所以这个 count 的 dep 又会把 渲染watcher 存放进自身的 subs 中。(
此时的count的dep:
{subs: [ sum的计算watcher,渲染watcher ]
}
所有此时的count更新时,就相当于把 subs 里保存的 watcher 依次去调用它们的 update 方法。
也就是依次调用 计算watcher 的 update 和 调用 渲染watcher 的 update
// Watcher 中update的源码实现
update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {/*同步则执行run直接渲染视图*/this.run()} else {/*异步推送到观察者队列中,由调度者调用。*/queueWatcher(this)}}
计算watcher 的 update就相当于下面的代码:
update () {if (this.lazy) {this.dirty = true}
}
就仅仅是把 计算watcher 的 dirty 属性置为 true,静静的等待下次读取即可。
渲染watcher 的 update代码
这里其实就是调用 this.run() ,重新根据 render 函数生成的 vnode 去渲染视图了,而在 render 的过程中,一定会访问到 sum 这个值,那么又回到 sum 定义的 get 上:
Object.defineProperty(vm, 'sum', { get() {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {// ?上一步中 dirty 已经置为 true, 所以会重新求值if (watcher.dirty) {watcher.evaluate()}if (Dep.target) {watcher.depend()}// 最后返回计算出来的值return watcher.value}}
})
由于上一步中的响应式属性更新,触发了 计算 watcher 的 dirty 更新为 true。 所以又会重新调用用户传入的 sum 函数计算出最新的值,页面上自然也就显示出了最新的值。
整个计算属性更新的流程就结束了,真的是好绕啊!!!
所以最后的流程是:
- 响应式的值 count 更新
- 同时通知 computed watcher 和 渲染 watcher 更新
- computed watcher 通过 update 方法把 dirty 设置为 true
- 视图渲染读取到 computed 的值,由于 dirty 所以 computed watcher 重新求值。
总结:
comoute初始化时取完值之后会将 dirty 设置为 false,这样以后如果只是取值的话都是直接读取缓存。
只有当计算属性依赖的响应式值发生更新的时候,才会把 dirty 重置为 true。然后重新计算compute的值。