在 Vue 官網(wǎng)文檔里面揽惹,對 computed
有這么一句描述:
計算屬性的結(jié)果會被緩存,除非依賴的響應式屬性變化才會重新計算。注意评疗,如果某個依賴 (比如非響應式屬性) 在該實例范疇之外母剥,則計算屬性是不會被更新的滞诺。
這句話非常重要,涵蓋了 computed
最關(guān)鍵的知識點:
-
computed
會搜集并記錄依賴环疼。 - 依賴發(fā)生了變化才會重新計算
computed
习霹,由于computed
是有緩存的,所以當依賴變化之后炫隶,第一次訪問computed
屬性的時候淋叶,才會計算新的值。 - 只能搜集到響應式屬性依賴伪阶,無法搜集到非響應式屬性依賴煞檩。
- 無法搜集到當前
vm
實例之外的屬性依賴。
如果僅局限于知道上述規(guī)則望门,而不理解內(nèi)部機制形娇,那么在實際開發(fā)中難免步步驚心,不敢甩手大干筹误。
Vue 內(nèi)部是怎么知道 computed
依賴的桐早?
對于在 RequireJS
時代摸爬滾打過不少年的同學來說,可能一下就會聯(lián)想到 RequireJS
獲取依賴的原理,使用 Function.prototype.toString()
方法將函數(shù)轉(zhuǎn)換成字符串哄酝,然后借助正則從字符串中查找 require('xxx')
這樣的代碼友存,最終分析出來依賴。
這種方式實際上有非常多的限制的:
- 如果在注釋里面出現(xiàn)了
require('xxx')
陶衅,豈不是會匹配出多余的依賴屡立。 - 在開發(fā)中,描述依賴的時候搀军,必須要寫成
require('xxxx')
的形式膨俐,require
中的字符串參數(shù)不能是各種動態(tài)的、復雜的字符串拼接罩句,否則就無法解析了焚刺。
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
數(shù)組蔓姚,訪問 vm.c
的時候,會調(diào)用相應的 get()
方法慨丐,在 get()
方法中坡脐,訪問了 this.obj.a
,而對于 this.obj.a
的訪問房揭,又會調(diào)用相應的 get
方法挨措,在該 get
方法中,有一句代碼 this.dependencies.push('a')
崩溪,往 vm.dependencies
中放置了當前執(zhí)行流程中依賴到的屬性浅役,然后以此類推,在 vm.c
訪問結(jié)束之后伶唯, vm.dependencies
里面就記錄了 vm.c
的依賴 ['a', 'b']
了觉既。
到這里,有的同學可能會產(chǎn)生新的疑問:如果在 vm.obj.a
中出現(xiàn)條件分支語句乳幸,豈不是會出現(xiàn)依賴搜集不完整的情況瞪讼?且看如下修改后的代碼:
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
發(fā)生變化的時候溜畅,才應該去重新計算 vm.c
。第二次訪問 vm.c
极祸,在 vm.obj.d
變?yōu)?true
之后慈格,就能搜集到依賴為 a
、 d
、 b
,此時重新掉之前的依賴項糠聪,后續(xù)按照新的依賴項來標記 vm.c
是否應該重新計算徙硅。
緩存
在得知 computed
屬性發(fā)生變化之后, Vue 內(nèi)部并不立即去重新計算出新的 computed
屬性值耀怜,而是僅僅標記為 dirty
,下次訪問的時候,再重新計算页眯,然后將計算結(jié)果緩存起來。
這樣的設計厢呵,會避免一些不必要的計算窝撵,比如有以下 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
碌奉,雖然后續(xù)通過 setInterval
不停地修改 this.a
短曾,造成 this.c
一直是 dirty
狀態(tài),但是由于并沒有再訪問 this.c
道批,所以重新計算 this.c
的值是毫無意義的错英,如果不做無意義的計算反倒會提升一些性能。
記錄的響應式屬性都在當前實例范疇內(nèi)
舉個例子:
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);
從上述例子可以發(fā)現(xiàn)隆豹, Child
組件輸出的 a
是不斷變化的椭岩,而 App
組件輸出的 b
是一直不會有什么內(nèi)容的。
這應該是 Vue 的一種設計策略璃赡,開發(fā)當前組件的時候判哥,就關(guān)注當前組件的數(shù)據(jù)就行了,不要牽連到其他地方的數(shù)據(jù)碉考,不然會增加耦合度塌计,和組件的解耦合初衷相違背。