概述
說到 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 把計算屬性變成響應式的話,計算屬性會執行自己的依賴,從而和響應式數據的依賴重復了。其實這也是把非數據變成響應式的一種方法。
