在 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 的一種設計策略,開發當前組件的時候,就關注當前組件的數據就行了,不要牽連到其他地方的數據,不然會增加耦合度,和組件的解耦合初衷相違背。