「一個人的一生會經歷三次成長:
第一次,是明白事情的對與錯。
第二次,是明白有些事不只對與錯。
第三次,是明白有些事沒有對錯之後,仍然堅定地去做自己相信的事情,並為之負起責任。」——米哈遊
書接上回,我們在/src/vue/baseHandler.ts寫了reactive返回的代理對象的get方法,其中調用/src/vue/effect.ts暴露的track方法,將對象本身、觸發get的屬性及當前正在作用的effect做雙層映射,以記錄每個reactive對象的key分別在哪些effect函數作用時被觸發,也就是所謂依賴收集。
讓我們先回顧一下/src/vue/effect.ts依賴收集相關的程式碼吧。
type Dep = Set<ReactiveEffect>;
type DepsMap = Map<string | symbol, Dep>;
type TargetMap = WeakMap<object, DepsMap>;
// type TargetMap = WeakMap<
// object,
// Map<
// string | symbol, Set<ReactiveEffect>
// >
// >
const targetMap: TargetMap = new WeakMap();
const trackEffects = (dep: Dep) => {
if (activeEffect && !dep.has(activeEffect)) {
dep.add(activeEffect);
}
};
export const track = (target: object, key: string | symbol) => {
if (!activeEffect) return;
let depsMap: DepsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, depsMap = new Map());
let dep: Dep = depsMap.get(key);
if (!dep) depsMap.set(key, dep = new Set());
trackEffects(dep);
};
既然我們能將一個reactive的key的所有依賴都收集到TargetMap的target的key所指向的這個Set中,我們只要遍歷這個Set,並依序執行這些ReactiveEffect對象的run方法,便能將這個reactive的key所收集的依賴,也就是跟它有關的組件全部重新渲染。
也就是說,接下來我們只要在effect.ts再暴露一個方法,去遍歷調用reactive的key映射的ReactiveEffect對象,並依序調用他們的run方法,最後再給reactive的set調用這個方法,數據驅動視圖就完成了!
先給/src/vue/effect.ts加上以下程式碼:
const triggerEffect = (effects: Dep) => {
effects.forEach(effect => {
if (effect !== activeEffect) {
effect.run();
}
});
};
export const trigger = (target: object, key: string | symbol) => {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) {
triggerEffect(effects);
}
};
先從暴露的trigger方法看起,首先trigger方法接收一個target跟一個key,分別代表觸發set的對象及被賦值導致set觸發的key。
拿到了target跟key,就檢查看看這個target是否有被targetMap收集:
const depsMap = targetMap.get(target);
if (!depsMap) return;
如果有被收集到,就檢查這個depsMap是否有收集到被更新的key,若有,便調用triggerEffect方法:
const effects = depsMap.get(key);
if (effects) {
triggerEffect(effects);
}
而這個triggerEffect方法就簡單了,只要收集到的effect不是當前的activeEffect,就調用它的run方法,可以理解成重新渲染它所對應的組件。
(畢竟activeEffect的run方法本來就會被調用,沒必要調兩次。)
最後再到/src/vue/baseHandler.ts把這個trigger方法在set中調用:
import { track, trigger } from './effect';
// ......
export default {
// ......
set: (target: Record<string, any>, key: string, value: any) => {
target[key] = value;
trigger(target, key);
return true;
}
};
就大功告成啦!趕緊到/src/index.ts隨便寫點甚麼測試看看好不好用。
import './assets/css/style.less';
import { reactive } from './vue';
import effect from './vue/effect';
const person = reactive({
name: '自尤雨溪望',
msg: '我想準時下班'
});
effect(() => {
document.body.innerHTML = `
<div>
<h5>${person.name}</h5>
<p>${person.msg}</p>
</div>
`;
});
setInterval(() => {
person.msg += '!';
}, 3000);
過啦~
當然目前的寫法仍是效能巨差,因為我們直接把整個body裡面的內容都重新渲染了。
按照計畫,之後我們會講ast抽象語法樹將template解析為虛擬dom,再用diff算法比對新舊虛擬dom,以實現渲染最少量的dom元素來更新視圖,不過這都是講完ref、computed、watch那些之後的事了。而且也要30天內能講得完才行ㄏ
githubmain分支commit「[Day 05]驅動視圖」