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 代码:




第一次访问 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: '
{{ a }}
' }); const App = { el: '#app', template: '
{{ b }} -
', 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

你可能感兴趣的:(Vue 内部是怎么知道 computed 依赖的?)