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

從上述代碼中看到,第一處依賴項輸出只有 ad ,并不是我們初步期望的是 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 之后,就能搜集到依賴為 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 的時候,記錄了依賴項 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 的一種設計策略,開發當前組件的時候,就關注當前組件的數據就行了,不要牽連到其他地方的數據,不然會增加耦合度,和組件的解耦合初衷相違背。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.安裝 可以簡單地在頁面引入Vue.js作為獨立版本,Vue即被注冊為全局變量,可以在頁面使用了。 如果希望搭建...
    Awey閱讀 11,074評論 4 129
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,868評論 18 139
  • 下載安裝搭建環境 可以選npm安裝,或者簡單下載一個開發版的vue.js文件 瀏覽器打開加載有vue的文檔時,控制...
    冥冥2017閱讀 6,081評論 0 42
  • 現在網上有很多早起打卡,早起俱樂部之類的活動。當然一些健身的App也有打卡的功能。還有一些習慣養成的App,比如;...
    青木俠閱讀 2,724評論 4 0
  • 1、市場分析(關鍵詞:眾籌、粉絲經濟) 2、用戶分層,用戶需求,產品策略/定位 3、產品功能邏輯,交互體驗 4、競...
    韋歌wege閱讀 2,465評論 0 9