Vue 内部是怎么知道 computed 依赖的?

在 Vue 官网文档里面,对 computed 有这么一句描述:

计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。注意,如果某个依赖 (比如非响应式属性) 在该实例范畴之外,则计算属性是不会被更新的。

这句话非常重要,涵盖了 computed 最关键的知识点:

  • computed 会搜集并记录依赖。
  • 依赖发生了变化才会重新计算 computed ,由于 computed 是有缓存的,所以当依赖变化之后,第一次访问 computed 属性的时候,才会计算新的值。
  • 只能搜集到响应式属性依赖,无法搜集到非响应式属性依赖。
  • 无法搜集到当前 vm 实例之外的属性依赖。

如果仅局限于知道上述规则,而不理解内部机制,那么在实际开发中难免步步惊心,不敢甩手大干。

Vue 内部是怎么知道 computed 依赖的?

对于在 RequireJS 时代摸爬滚打过不少年的同学来说,可能一下就会联想到 RequireJS 获取依赖的原理,使用 Function.prototype.toString() 方法将函数转换成字符串,然后借助正则从字符串中查找 require('xxx')这样的代码,最终分析出来依赖。

这种方式实际上有非常多的限制的:

  • 如果在注释里面出现了 require('xxx') ,岂不是会匹配出多余的依赖。
  • 在开发中,描述依赖的时候,必须要写成require('xxxx')的形式,require 中的字符串参数不能是各种动态的、复杂的字符串拼接,否则就无法解析了。

Vue 显然没有使用这么低效不准确的方式。

我们可以先看一段伪代码:

const vm = {
    dependencies: [],
    
    obj: {
        get a() {
            // this 指向 vm 对象。
            this.dependencies.push('a');
            return this.obj.b;
        },
        get b() {
            this.dependencies.push('b');
            return 1;
        }
    },
    computed: {
        c: {
            get() {
                // this 指向 vm 对象。
                return this.obj.a;
            }
        }
    }
};

vm.dependencies = [];
console.log(vm.c);
console.log('vm.c 依赖项:', vm.dependencies); // 输出: vm.c 依赖项: a, b

在上述代码中,访问 vm.c 之前,清空了一下 vm.dependencies 数组,访问 vm.c 的时候,会调用相应的 get() 方法,在 get()方法中,访问了 this.obj.a,而对于 this.obj.a的访问,又会调用相应的 get 方法,在该 get方法中,有一句代码 this.dependencies.push('a') ,往 vm.dependencies 中放置了当前执行流程中依赖到的属性,然后以此类推,在 vm.c 访问结束之后, vm.dependencies里面就记录了vm.c的依赖 ['a', 'b']了。

到这里,有的同学可能会产生新的疑问:如果在 vm.obj.a 中出现条件分支语句,岂不是会出现依赖搜集不完整的情况?且看如下修改后的代码:

const vm = {
    dependencies: [],
    
    obj: {
        get a() {
            // this 指向 vm 对象。
            this.dependencies.push('a');
            if (this.obj.d) {
                return this.obj.b;
            }
            
            return 2;
        },
        get b() {
            this.dependencies.push('b');
            return 1;
        },
        get d() {
            this.dependencies.push('d');
            return this._d;
        },
        set d(val) {
            this._d = val;
        }
    },
    computed: {
        c: {
            get() {
                // this 指向 vm 对象。
                return this.obj.a;
            }
        }
    }
};

vm.dependencies = [];
vm.obj.d = false;
console.log(vm.c);
console.log('vm.c 依赖项:', vm.dependencies); // 输出: vm.c 依赖项: a, d

vm.dependencies = [];
vm.obj.d = true;
console.log(vm.c);
console.log('vm.c 依赖项:', vm.dependencies); // 输出: vm.c 依赖项: a, d, b

从上述代码中看到,第一处依赖项输出只有 a 、 d ,并不是我们初步期望的是 a 、 d 、 b 。

实际上,这并不会带来什么问题,相反,还能在一些场景下提升性能,为什么这么说呢?

在第一次访问 vm.c的时候,虽然只记录了a 、 d,两个依赖项,但是并不会引起 bug ,表面上看此时 vm.obj.b变化了,应该重新计算 vm.c的值,但是由于vm.obj.d还是 false ,所以 vm.obj.a的值并不会改变,因此vm.c的值也不会改变,所以重新计算 vm.c并没有意义。所以在这个时候,只有 a 、 d发生变化的时候,才应该去重新计算vm.c。第二次访问 vm.c ,在 vm.obj.d变为true之后,就能搜集到依赖为a 、 d 、 b,此时重新掉之前的依赖项,后续按照新的依赖项来标记 vm.c是否应该重新计算。

缓存

在得知 computed 属性发生变化之后, Vue内部并不立即去重新计算出新的 computed 属性值,而是仅仅标记为dirty,下次访问的时候,再重新计算,然后将计算结果缓存起来。

这样的设计,会避免一些不必要的计算,比如有以下 Vue 代码:

<template>
    <div class="my-component">
        ...
    </div>
</template>

<script>
export default {
    data() {
        return {
            a: 1,
            b: 2
        };
    },
    computed: {
        c() {
            return this.a + this.b;
        }
    },
    created() {
        console.log(this.c);
        
        setInterval(() => {
            this.a++;
        },1000);
    }
};
</script>

第一次访问 this.c的时候,记录了依赖项 a 、 b,虽然后续通过setInterval不停地修改this.a,造成 this.c 一直是 dirty状态,但是由于并没有再访问 this.c ,所以重新计算this.c 的值是毫无意义的,如果不做无意义的计算反倒会提升一些性能。

记录的响应式属性都在当前实例范畴内

举个例子:

import Vue from 'vue';

Vue.component('Child', {
    data() {
        return {
            a: 1
        };
    },
    created() {
        setInterval(() => {
            this.a++;
        }, 1000);
    },
    template: '<div>{{ a }}</div>'
});

const App = {
    el: '#app',
    template: '<div>{{ b }} - <Child ref="child" /></div>',
    computed: {
        b() {
            return this.$refs.child && this.$refs.child.a;
        }
    }
};

new Vue(App);

从上述例子可以发现, Child 组件输出的 a 是不断变化的,而 App 组件输出的 b 是一直不会有什么内容的。

这应该是 Vue 的一种设计策略,开发当前组件的时候,就关注当前组件的数据就行了,不要牵连到其他地方的数据,不然会增加耦合度,和组件的解耦合初衷相违背。


from:https://www.jianshu.com/p/b38f826f42bc

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

推荐阅读更多精彩内容

  • 在 Vue 官网文档里面,对 computed 有这么一句描述: 计算属性的结果会被缓存,除非依赖的响应式属性变化...
    yibuyisheng阅读 18,092评论 1 10
  • 33、JS中的本地存储 把一些信息存储在当前浏览器指定域下的某一个地方(存储到物理硬盘中)1、不能跨浏览器传输:在...
    萌妹撒阅读 2,014评论 0 2
  • 基本介绍 话不多说,一个最基本的例子如下: Vue中我们不需要在template里面直接计算{{this.firs...
    指尖跳动阅读 2,908评论 0 1
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,855评论 1 4
  • 前言 使用Vue在日常开发中会频繁接触和使用生命周期,在官方文档中是这么解释生命周期的: 每个 Vue 实例在被创...
    心_c2a2阅读 2,138评论 1 8