Vue2.x計算屬性為什麼能依賴於另一個計算屬性
概述
說到 computed 和 watch 有什麼不同,也許大多數人都知道:computed 是用現有資料生成一個新資料,並且能夠被快取;而 watch 是根據資料變化,執行一些回撥函式,它有很多配置比如 deep、immediate 等。
大家也都知道,watch 只是原始碼裡面 watcher 的一個例項,computed 屬性也用到了 watcher,但是 computed 屬性為什麼能夠相互依賴變化呢?明顯 watcher 自己是做不到這一點的,因為 watcher 並不能 update 其它 watcher。我為了弄懂其中的原理根據 vue2.x 的原始碼寫了一個簡易的 computed 屬性,供以後工作時參考,相信對其他人也有用。
部分程式碼來源於Vue2.x是怎麼收集依賴的
簡易的 computed
為了簡便,暫不考慮 computed 的 setter 的情況,我實現了一個簡易的 computed,程式碼如下:
function defineReactive(obj, key, val) { const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { if (Dep.target) { dep.depend(); } return val; }, set(newVal) { val = newVal; dep.notify(); } }); } class Dep { constructor() { this.subs = []; } addSub(sub) { this.subs.push(sub); } removeSub() { const index = this.subs.indexOf(sub); if (index > -1) { this.subs.splice(index, 1); } } depend() { if (Dep.target) { Dep.target.addDep(this); } } notify() { const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); } } } Dep.target = null; const targetStack = [] function pushTarget (target) { targetStack.push(target) Dep.target = target } function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } class Watcher { constructor(cb, dirty = false) { this.getter = cb; this.deps = []; this.newDeps = []; this.value = this.get(); this.dirty = dirty; } get() { pushTarget(this); const value = this.getter(); popTarget(this); this.deps = [...this.newDeps]; this.newDeps = []; return value; } addDep(dep) { this.newDeps.push(dep); dep.addSub(this); } update() { this.dirty = true; this.value = this.get(); } evaluate() { this.value = this.get(); this.dirty = false; } depend() { let i = this.deps.length; while (i--) { this.deps[i].depend(); } } } const obj = {}; defineReactive(obj, 'text', 'Hello World!'); const vm = {}; const computed = { text1() { return `${obj.text}-text1`; }, text2() { return `${vm.text1}-text2`; } }; function createComputedGetter(key) { return function computedGetter() { const watcher = vm.computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value; } } } function defineCompute(target) { const watchers = vm.computedWatchers = Object.create(null); for (key in target) { const cb = target[key]; watchers[key] = new Watcher(cb, true); //defineComputed Object.defineProperty(vm, key, { get: createComputedGetter(key), set(a) { return a; } }); } } defineCompute(computed); const watcher = new Watcher(() => { document.querySelector('body').innerHTML = vm.text2; });
把上面的程式碼複製到瀏覽器的控制檯執行,就可以看到瀏覽器裡面出現了Hello World-text1-text2
,然後我們繼續在控制檯輸入obj.text = 'Define Reactive'
,可以看到瀏覽器裡面的Hello World-text1-text2
就變成了Define Reactive-text1-text2
。
顯然,由於我們改變了obj.text
的值,然後自動的導致了vm.text1
和vm.text2
的值發生了響應式變化。
而其中的原理是,假如計算屬性 A 依賴計算屬性 B,而計算屬性 B 又依賴響應式資料 C,那麼最一開始先把計算屬性 AB 都轉化為 watcher,然後在把計算屬性 AB 掛載到 vm 上面的時候,插入了一段 getter,而計算屬性 B 的這個 getter 在這個計算屬性 B 被讀取的時候會把計算屬性 A 的 watcher 新增到響應式資料 C 的依賴裡面,所以響應式資料 C 在改變的時候會先後導致計算屬性 B 和 A 執行 update,從而發生改變。
而其中關鍵的那段程式碼就是這段:
function createComputedGetter(key) {
return function computedGetter() {
const watcher = vm.computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
// 這裡非常關鍵
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
}
}
為什麼在計算屬性 B 的 getter 函式裡面會新增計算屬性 A 的 watcher 呢?這是因為計算屬性 B 在求值完成後,會自動把Dep.target
出棧,從而暴露出計算屬性 A 的 watcher。程式碼如下:
class Watcher {
get() {
// 這裡把自己的 watcher 入棧
pushTarget(this);
const value = this.getter();
// 這裡把自己的 watcher 出棧
popTarget(this);
this.deps = [...this.newDeps];
this.newDeps = [];
return value;
}
}
這就是 pushTarget 和 popTarget 排程 watchers 的美麗之處~~
其它
需要注意以下兩點:
1.在給計算屬性生成 getter 的時候,不能直接使用 Object.defineProperty,而是使用閉包把 key 值儲存了起來。
2.為什麼不直接使用 defineReactive 把計算屬性變成響應式的。因為當把計算屬性用 setter 掛載到 vm 上面的時候,計算屬性這裡確實變成了一個具體的值,但是如果使用 defineReactive 把計算屬性變成響應式的話,計算屬性會執行自己的依賴,從而和響應式資料的依賴重複了。其實這也是把非資料變成響應式的一種方法。