[Vue.js进阶]从源码角度剖析计算属性的原理

image

前言

最近在学习Vue计算属性的源码,发现和普通的响应式变量内部的实现还有一些不一样,特意写了这篇博客,记录下本身学习的成果vue

文中的源码截图只保留核心逻辑 完整源码地址git

可能须要了解一些Vue响应式的原理github

Vue 版本:2.5.21数组

计算属性的概念

通常的计算属性值是一个函数,这个函数会返回一个值,而且其函数内部还可能会依赖别的变量缓存

通常的计算属性看起来和 method 很像,值都是一个函数,那他们有什么区别呢闭包

计算属性和method的区别

将一个计算属性的函数放在 methods 中一样也能达到相同的效果app

可是若是视图中依赖了这个 method 的返回值,而且当另一个其余的响应式变量的修改致使视图被更新时, method 会从新执行一遍,即便此次的更新和 method 中依赖的变量没有任何关系!异步

而对于计算属性,只有当计算属性依赖的变量改变后,才会从新执行一遍函数,并从新返回一个新的值函数

点我看示例性能

当 otherProp 变量被修改致使更新视图的时候,methodFullName 每次都会执行,而 computedFullName 只会在页面初始化的时候执行一次,Vue 推荐开发者将 method 和 compute 属性区分开来,可以有效的提高性能,避免执行一些没必要要的代码

回顾过计算属性的概念,接下来咱们深刻源码,来了解一下计算属性究竟是怎么实现的,为何只有计算属性的依赖项被改变了才会从新求值

从例子入手

这里我写了一个简单的例子,帮助各位理解计算属性的运行原理,下面的解析会围绕这个例子进行解析

const App = {
    template: ` <div id="app"> <div>{{fullName}}</div> <button @click="handleChangeName">修改lastName</button> </div> `,
    data() {
        return {
            firstName: '尤',
            lastName: '雨溪',
        }
    },
    methods: {
        handleChangeName() {
            this.lastName = '大大'
        }
    },
    computed: {
        fullName() {
            return this.firstName + this.lastName
        }
    }
}

new Vue({
    el: '#app',
    components: {
        App
    },
    template: ` <App></App> `
}).$mount()
复制代码

fullName 依赖了 firstName 和 lastName,点击 button 会修改 lastName, 同时 fullName 会从新计算,视图变成"尤大大"

深刻计算属性的源码

在平常开发中书写的计算属性,实际上内部都会保存一个 watcher, watcher 的做用是观察某个响应式变量的改变而后执行相应的回调,由 Watcher 类实例化而成, Vue 中定义了3个 watcher

  • render watcher: 模板依赖而且须要显示在视图上变量,其内部保存了一个 render watcher
  • computed watcher: 计算属性内部保存了一个 computed watcher
  • user watcher: 使用 watch 属性观察的变量内部保存了一个 user watcher

理解这3个 watcher 各自的做用很是重要,文本会着重围绕 computed watcher 展开

一个计算属性的初始化分为2部分

  1. 实例化一个 computed watcher
  2. 定义计算属性的 getter 函数

生成computed watcher

在初始化当前组件时,会执行 initComputed 方法初始化计算属性,会给每一个计算属性实例化一个 computed watcher

在实例化 watcher 时传入不一样的配置项就能够生成不一样的 watcher 实例 ,当传入{ lazy: true } 时,实例化的 watcher 即为 computed watcher

定义计算属性的 getter 函数

在建立完 computed watcher 后,接着会定义计算属性的 getter 函数,咱们在执行计算属性的函数时,实际上执行的是 computedGetter 这个函数

computedGetter代码不多,可是倒是计算属性的核心,咱们一步步来分析

dirty属性

经过 key 获取到第一步中定义的 computed watcher,随后会判断这个 computed watcher 的 dirty 属性是否为 true,当 dirty 为 true 时, 会执行 evaluate 方法, evaluate 内部会执行计算属性的函数,而且将 watcher 的 value 属性等于函数执行后的结果也就是最终计算出来的值,具体咱们放到后面讲

dirty 属性是一个用来检测当前的 computed watcher是否须要从新执行的一个标志,这也是计算属性和普通method的区别,结合上图能够发现,当 dirty 为 false 时,就不会去执行 evaluate 也就不会执行计算属性的函数,能够看到最后直接就返回了 watcher.value 表示此次不会进行计算,会直接使用之前的 value 的值

当第一次触发computedGetter 时,dirty 属性的默认值是 true ,那是由于在初始化 computed watcher时候 Vue 将 dirty 属性等于了 lazy 属性,即为 true

知道 dirty 的默认值为 true,何时为 false 呢?咱们接着来看 evalutate 具体的实现

evaluate方法

evaluate 方法是 computed watcher 独有的方法,代码也只有短短2行

get方法

第一行执行了 get 方法, get 方法是全部 watcher 用来求值的通用方法

get 主要就作了这三步

  1. 将当前这个 watcher 做为栈顶的 watcher 推入栈
  2. 执行getter方法
  3. 将这个 watcher 弹出栈

咱们知道 Vue.js 会维护一个全局的栈用来存放 watcher ,每当触发响应式变量内部的 getter 时,就会收集这个全局的栈的顶部的 watcher(即Dep.target),将这个 watcher 存入响应式变量内部保存的dep中

第一步经过 pushTarget 将当前的 computed watcher 推入全局的栈中,此时Dep.target就指向这个栈顶的 computed watcher

第二步执行 getter 方法, 对于 computed watcher,getter 方法就是计算属性的函数,执行函数将返回的值赋值给 value 属性,而当计算属性的函数执行时,若是内部含有其余的响应式变量,会触发它们内部的 getter ,将第一步放入做为当前栈顶的 computed watcher 存入响应式变量内部的dep对象中

注意响应式变量内部的 getter 和 getter 方法不是一个函数

第三步将这个 computed watcher 弹出全局的栈

之因此将这个 computed watcher 推入又弹出,是为了让第二步执行内部的 getter 时,能让计算属性函数内部依赖的响应式变量收集到这个 computed watcher

对于计算属性来讲,get 方法的做用就是进行求值

将dirty设为false

执行完 get 方法,即一旦计算属性执行过一次求值,就会将 dirty 属性设为 false,若是下次又触发了这个计算属性的 getter 会直接跳过求值阶段

结合🌰

在例子中,由于视图须要依赖 fullName 这个响应式变量,因此会触发它的内部的 getter,同时它又是一个计算属性,即会执行 computedGetter ,此时 dirty 属性为默认值 true,执行 evaluate => get => pushTarget

pushTarget 中,因为是 computed watcher 执行的 get 方法,因此 this 指向这个 computed watcher, 将它推入全局栈中做为 Dep.target,随后执行计算属性的函数

能够看到计算属性 fullName 的函数依赖了 firstName 和 lastName 这2个响应式变量,Vue在内部经过闭包的形式各自保存了一个 dep 对象,这个 dep 对象会收集当前栈顶的 watcher,即收集 fullName 这个计算属性的 computed watcher,因此当计算属性的函数执行完毕后,firstName 和 lastName 内部的 dep 对象中都会保存一个 computed watcher

收集完毕后,将 computed watcher 弹出,让栈恢复到以前的状态

depend方法

计算属性第二个特色就是它的 depend 方法,这个方法是 computed watcher 独有的

当 Dep.target 存在,说明在上一步弹出了 computed watcher 后全局的栈中仍有其余的 watcher。好比当视图中依赖了当前的计算属性,那当前栈顶的 watcher 就是 render watcher,亦或者另一个计算属性内部依赖了当前的计算属性,那栈顶的 watcher 多是另外一个 computed watcher,无论怎么说只要有地方使用到这个计算属性,就会进入 depend 方法

watcher 的 depend 方法:

depend 方法也很是简短,它会遍历当前 computed watcher 的deps属性,依次执行 dep 的 depend 方法

deps 又是什么呢,前面说到 dep 是每一个响应式变量内部保存的一个对象,deps 可想而知就是全部响应式变量内部 dep 的集合,那具体是哪些响应式变量呢?其实了解过响应式原理的朋友应该知道,这个 deps 实际上保存了全部收集了当前 watcher 的响应式变量内部的 dep 对象

这是一个互相依赖的关系,每一个响应式变量内部的 dep 会保存全部的 watchers,而每一个 watcher 的 deps 属性会保存全部收集到这个 watcher 的响应式变量内部的 dep 对象

(Vue之因此在 watcher 中保存 deps,一方面须要让计算属性可以收集依赖,另外一方面也能够在注销这个 watcher 时能知道哪些 dep 依赖了这个 watcher,只须要调用 dep 里对应的注销方法便可)

接着就会遍历每一个 dep 执行 dep.depend 方法:

这个方法的做用是给当前的响应式变量内部的 dep 收集当前栈顶的 watcher ,在例子中,由于视图中依赖了 fullName,因此当 get 方法执行结束 computed watcher 被弹出后,栈顶的 watcher 就变为原来的 render watcher

computed watcher 中的 deps 属性保存了2个 dep,一个是 firstName 的 dep,另外一个是 lastName 的 dep,由于这2个变量在执行 get 方法第二步的时候收集了到这个 computed watcher

这时候执行 dep.depend 时会再次给这2个响应式变量收集栈顶的 watcher,即 render watcher,最终这2个变量内部的 dep 都保存了2个变量,一个 computed watcher,一个 render watcher

最终返回 watcher.value 做为显示在视图中的值

修改计算属性依赖的变量

前面说过,只有当计算属性的依赖项被修改时,计算属性才会从新进行计算,生成一个新的值,而视图中其余变量被修改致使视图更新时,计算属性不会从新计算,这是怎么作到的呢?

当计算属性的依赖项,即 firstName 和 lastName 被修改时,会触发内部的 setter,Vue 会遍历响应式变量内部的 dep 保存的 watcher,最终会执行每一个 watcher 的 update 方法

能够看到 update 方法有3种状况:

  • lazy:只存在于 computed watcher
  • sync:只存在于 user watcher,当 user watcher 设置了 sync 会同步调用 watcher 不会延迟到 nextTick 后,基本不会用
  • 默认状况:通常的 user watcher 和 render watcher 都会执行 queueWatcher,将这些 watcher 放到 nextTick 后执行

经过前面的 evaluatedepend 方法,firstName 和 lastName 内部的 dep 中都会保存2个 watcher,一个 computed watcher,一个 render watcher,当 lastName 被修改时,会触发内部的 setter,遍历 dep 保存的全部 watchers,这里会先执行 computed watcher 的 update 方法

同时前面说到在 computed watcher 求值结束后,会将 dirty 置为 false,以后再获取计算属性的值时都会跳过 evaluate 方法直接返回之前的 value,而执行 computed watcher 的 update 方法会将 dirty 再次变成 true,整个computed watcher 只作这一件事,即取消 computed watcher 使用之前的缓存的标志

这个操做是同步执行的,也就是说即便 render watcher 或 user watcher 在 watchers 数组中比 computed watcher 靠前,可是因为前2个 watcher 通常是异步执行的,因此最终执行的时候 computed watcher 会优先执行

真正的求值操做是在 render watcher 中进行的,当遍历到第二个 render watcher 时,因为视图依赖了 fullName,会触发计算属性的 getter,再次执行以前的 computedGetter,此时因为上一步将 dirty 变成 true了,因此就会进入 evalutate 从新计算,此时 fullName 就拿到了最新的值"尤大大"了

修改非计算属性依赖的变量

回到一开始计算属性和 method 区别的那个例子,由于视图依赖了 otherProp 因此当这个响应式变量被修改时,会触发它内部 dep 保存的 render watcher 的 update 方法,它会从新收集依赖更新视图

当收集到 methodFullName 时,由于是一个普通的 method,每次视图更新 Vue 都会执行相应的方法,因此每次都会打印 "method",而当收集 computedFullName 时,会执行 computedGetter,可是由于 otherPorp 不是这个计算属性依赖的变量,没有触发过 computed watcher 的 update,因此 dirty 属性为 false,就会跳过evaluate 方法直接返回缓存的结果,所以不会每次打印 "computed"

总结

只有当计算属性依赖的响应式变量被修改时,才会使得计算属性被从新计算,不然使用的都是第一次的缓存值,缘由是由于计算属性内部的 computed watcher 的 dirty 属性若是为 false 就会始终使用之前缓存的值

而计算属性依赖的响应式变量内部的 dep 都会保存这个 computed watcher,当它们被修改时,会触发 computed watcher 的 update 方法,将 dirty 标志位置为 true,这样下次有别的 watcher 依赖这个计算属性时就会触发从新计算

参考资料

Vue.js 技术揭秘