计算属性缓存原理

示例

new Vue({
    data() {
        return {
            count: 1,
            ourder: hello,
        }
    },
    computed: {
        sum() {
            return this.count + 1
        },
    },
    methods: {
        change() {
            this.count = 2
        },
    },
})

回顾 watcher 的流程

watcher 的的核心概念是 get 求值和 update 更新

  • 在求值的时候,它会把自身,也就是 watcher 本身赋值给 Dep.target 这个全局变量
  • 然后求值的过程中,会读取到响应式属性,那么响应式属性的 dep 就会收集到这个 watcher 作为依赖
  • 下次响应式属性更新了,就会从 dep 中找出它收集到 watcher,触发 watcher.update() 去更新

在基本的响应式更新视图的流程中

  • get 求值就是指 Vue 的组件重新渲染的函数,
  • update 的时候,其实就是重新调用组件的渲染函数去更新视图

这套流程同样适用于 computed 的更新

初始化 computed

Vue 会对 options 中的每个 computed 属性也用 watcher 去包装起来,它的 get 函数显然就是要执行用户定义的求值函数,而 update 则是一个比较复杂的流程

初始化

在组件初始化的时候,会进入到初始化 computed 的函数

if (options.computed) {
    initComputed(vm, opts.computed)
}
// initComputed 函数
var watchers = (vm._computedWatchers = Object.create(null))
// 依次为每个 computed 属性定义
for (const key in computed) {
    const userDef = computed[key]
    watchers[key] = new Watcher(
        vm, // 实例
        getter, // 用户传入的求值函数
        noop, // 回调函数
        { lazy: true } // 声明 lazy 属性,标记 computed watcher
    )
    // 用户在调用 getter 函数的时候,会发生的事情
    defineComputed(vm, key, userDef)
}

调用循环为每个 computed 属性生成一个计算watcher,该 watcher 保留管家你属性简化后的形式为

{
    deps: [],
    dirty: true, // 缓存的关键
    getter: sum(), // 此处假设用户传入的函数为 sum()
    lazy: true,
    value: undefined
}

可以看到 value 开始为 undefined,lazy 为 true,说明它的值是惰性计算的,只有到真正在模板里去读取它的值后才会计算

接下来看看 defineComputed,它决定了用户在读取 this.sum 这个计算属性的值后会发生什么

// defineComputed 简化后的代码
Object.defineProperty(target, key, {
    get() {
        // 组件实例上拿到 computed watcher
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            // 只有 dirty 为 true 才会重新求值
            if (watcher.dirty) {
                watcher.evaluate()
            }
            // 此处也是个关键
            if (Dep.target) {
                watcher.depend()
            }
            // 返回计算出来的值
            return watcher.value
        }
    },
})

dirty 这个概念代表脏数据,说明这个数据需要重新调用用户传入的 sum 函数来求值,第一次在模板中读到的时候,它一定是 true,所以初始化的时候就会经历一次求值

// evaluate 函数
evaluate() {
    this.value = this.get()
    this.dirty = false
}

下次没有特殊情况时,再次读取到 sum 的时候,发现 dirty 为 false,则直接返回 value,这就是计算属性缓存的概念

更新

当 count 更新的时候,到底是怎么触发 sum 在页面上变更的?要从上面提到的 evaluate() 函数中的求值操作说起

Dep.target 变更为渲染watcher,此时 Dep.target 为渲染watcher,targetStack 为 [渲染 watcher]

在模板中读取 sum 变量的时候,全局的 Dep.target 应该是渲染watcher,全局的 Dep.target 状态是一个用栈 targetStack 来保存,便于前进和回退 Dep.target

// get函数
get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        value = this.getter.call(vm, m)
    } finally {
        popTarget()
    }
    return value
}

刚进入 get 方法的时候,就执行 pushTarget,把 计算watcher自身置为 Dep.target,等待依赖收集,执行完 pushTarget 后

Dep.target 变更为计算watcher,此时 Dep.target 为计算watcher,targetStack 为 [渲染 watcher, 计算 watcher]

代码往下,此时会执行 getter 函数,就是用户传入的 sum 函数,执行的时候读取到了 this.count,这是一个响应式的属性,此时会触发 count 的 get 劫持

// get 简化代码
// 在闭包中,会保留对于 count 这个 key 所定义的 dep
const dep = new Dep()
// 闭包中也会保留上一次 set 函数所设置的 val
let val
Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
        const value = vale
        // Dep.target 此时是 计算watcher
        if(Dep.target) {
            // 收集依赖
            dep.depend()
        }
        return value
    }
})

// dep.depend()
depend() {
    // 此时 Dep.target 为 计算watcher
    if(Dep.target) {
        Dep.target.addDep(this)
    }
}

// 计算watcher 的 addDep 函数
addDep(dep) {
    // 此处做了一系列去重操作,简化掉
    // 这里把 count 的 dep 也存在自身的 deps 上
    this.deps.push(dep)
    // 带着 计算watcher 自身作为参数,回到 dep 的 addSub 函数
    dep.addSub(this)
}

// Dep
class Dep {
    subs = []
    addSub(sub){
        this.subs.push(sub)
    }
}

经历上述的收集流程后,此时的一些状态,可以看出 计算watcher 和它所依赖的响应式值的 dep,互相保留了彼此

// sum 的 计算watcher
{
    deps: [count的dep],
    dirty: false, // 求值完了,所以是false
    value: 2,
    getter: sum(),
    lazy: true
}

// count 的 dep
{
    subs: [sum的计算watcher]
}

收集流程执行完,执行 poptarget

Dep.target 变更为渲染watcher,此时 Dep.target 为渲染watcher,targetStack 为 [渲染 watcher]

此时 evaluate 函数执行完毕,代码继续往下,走到了下面这里

if (Dep.target) {
    watcher.depend()
}

此时的 Dep.target 为 渲染watcher,所以进入到了 watcher.depend()

// watcher.depend()
depend() {
    let i = this.deps.length
    while(i--){
        this.deps[i].depend()
    }
}

由于经过前面的收集依赖过程,计算watcher的 deps 里面保存了 count 的 dep,也就是说此时又会调用 count 上的 dep.depend()

// Dep
class Dep {
    subs = []
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
}

这次的 Dep.target 已经是 渲染watcher 了,所以这个 count 的 dep 又会把 渲染watcher 存放进自身的 subs 中

{
    subs: [sum的计算watcher, 渲染watcher]
}

此时 count 更新了,再回到 count 的响应式劫持逻辑里去

const dep = new Dep()
let val
Object.defineProperty(obj, key, {
    set: function reactiveSetter(newVal) {
        val = newVal
        // 触发 count 的 dep 的 notify
        dep.notify()
    },
})

// Dep
class Dep {
    subs = []
    notify() {
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

把 subs 里保存的 watcher 依次去调用它们的 update 方法

  1. 调用 计算watcher 的 update

    // 计算watcher 的 update
    update(){
        if(this.lazy) {
            this.dirty = true
        }
    }
    
  2. 调用 渲染watcher 的 update 这里其实就是调用 vm._update(vm._render()) 这个函数,重新根据 render 函数生成 vnode 去渲染视图

    而在 render 的过程中,一定会访问到 sum 这个值,那么又回到 sum 定义的 get 函数

缓存生效的情况

只有计算属性依赖的响应式值发生更新的时候,才会把 dirty 重置为 true,这一下次读取的时候才会发生真正的计算

以开头的示例来说,当 count 发生变换的时候,sum 才会重新计算,order 发生变换的时候,sum 读取的是缓存值

总结

计算属性更新的路径是为

  • 响应式的值 count 更新
  • 同时通知computed watcher渲染 watcher 更新
  • computed watcher 把 dirty 设置为 true
  • 视图渲染读到 computed 的值,由于 dirty 为 ture 所以 computed watcher 重新求值
Last Updated:
Contributors: af