Vue 从入门到进阶

computed 原理解析

2018-03-10  本文已影响89人  yibuyisheng

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

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

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

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

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

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

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

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

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

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

在第一次访问 vm.c 的时候,虽然只记录了 ad ,两个依赖项,但是并不会引起 bug ,表面上看此时 vm.obj.b 变化了,应该重新计算 vm.c 的值,但是由于 vm.obj.d 还是 false ,所以 vm.obj.a 的值并不会改变,因此 vm.c 的值也不会改变,所以重新计算 vm.c 并没有意义。所以在这个时候,只有 ad 发生变化的时候,才应该去重新计算 vm.c 。第二次访问 vm.c ,在 vm.obj.d 变为 true 之后,就能搜集到依赖为 adb ,此时重新掉之前的依赖项,后续按照新的依赖项来标记 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 的时候,记录了依赖项 ab ,虽然后续通过 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 的一种设计策略,开发当前组件的时候,就关注当前组件的数据就行了,不要牵连到其他地方的数据,不然会增加耦合度,和组件的解耦合初衷相违背。

上一篇下一篇

猜你喜欢

热点阅读