当前位置: 代码迷 >> 综合 >> 【Vue 源码学习笔记】-- Computed
  详细解决方案

【Vue 源码学习笔记】-- Computed

热度:79   发布时间:2023-12-13 07:39:18.0

一直都在网上查阅关于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 函数计算出最新的值,页面上自然也就显示出了最新的值。

整个计算属性更新的流程就结束了,真的是好绕啊!!!

所以最后的流程是:

  1. 响应式的值 count 更新
  2. 同时通知 computed watcher渲染 watcher 更新
  3. computed watcher 通过 update 方法把 dirty 设置为 true
  4. 视图渲染读取到 computed 的值,由于 dirty 所以 computed watcher 重新求值。

总结:

comoute初始化时取完值之后会将 dirty 设置为 false,这样以后如果只是取值的话都是直接读取缓存。

只有当计算属性依赖的响应式值发生更新的时候,才会把 dirty 重置为 true。然后重新计算compute的值。