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

image

前言

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

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

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

Vue 版本:2.5.21

计算属性的概念

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

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

计算属性和method的区别

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

但是如果视图中依赖了这个 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 方法的作用就是进行求值

🌰

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

pushTarget 中由于是 computed watcher 执行的 get 方法,所以 this 指向 这个 computed watcher 将它推入全局栈中,随后执行计算属性的函数

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

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

将dirty设为false

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

depend方法

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

当 Dep.target 存在,即全局的栈中仍有其他的 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,所以会优先执行 computed watcher 的 update 方法

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

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

其他变量的修改不会影响到计算属性

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

当收集 methodFullName 时会执行相应的方法,所以会打印 "method",当收集 computedFullName 时,会执行 computedGetter,但是此时没有触发过 computed watcher 的 update,所以 dirty 属性为 false,就会跳过evaluate 方法直接返回缓存的结果,因此不会打印 "computed"

总结

只有当计算属性依赖的响应式变量被修改时,才会使得计算属性被重新计算,否则使用的都是第一次的缓存值,原因是因为计算属性内部的 computed watcher 的 dirty 属性如果为 false 就会始终使用以前缓存的值

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345