vue3.0監(jiān)測機(jī)制有了很大的改善,彌補(bǔ)了vue2.0
的一些局限:
- 對屬性的添加孙咪、刪除動(dòng)作的監(jiān)測;
- 對數(shù)組基于下標(biāo)的修改夺刑、對于 .length 修改的監(jiān)測缅疟;
- 對 Map、Set遍愿、WeakMap 和 WeakSet 的支持存淫;
這里為了更好的理解原理手動(dòng)實(shí)現(xiàn)vue3.0的監(jiān)測對象屬性變化的Demo(實(shí)際源碼中還需考慮map, set等數(shù)據(jù)類型,這里僅用普通對象為例)
vue3.0 使用proxy代替了vue2.0版本中的defineProperty沼填,首先利用compositionAPI中的 reactive() 函數(shù)返回一個(gè)proxy對象桅咆,使得數(shù)據(jù)可監(jiān)測
// reactive() 函數(shù)接受一個(gè)普通對象 返回一個(gè)響應(yīng)式數(shù)據(jù)對象
function reactive(target) {
// 通過proxy將對象變?yōu)轫憫?yīng)式
const observed = new Proxy(target, baseHandler);
// 返回proxy代理后的對象
return observed;
}
Proxy 可以理解成,在目標(biāo)對象之前架設(shè)一層“攔截”坞笙,外界對該對象的訪問岩饼,都必須先通過這層攔截,因此提供了一種機(jī)制薛夜,可以對外界的訪問進(jìn)行過濾和改寫
var proxy = new Proxy(target, handler);
target參數(shù)表示所要攔截的目標(biāo)對象籍茧,handler參數(shù)也是一個(gè)對象,用來定制攔截行為梯澜。
baseHandler中定義攔截的get set方法寞冯,監(jiān)測和改寫數(shù)據(jù),為了方便晚伙,我們需要先將所有依賴收集起來吮龄,一旦數(shù)據(jù)發(fā)生變化,就統(tǒng)一通知更新咆疗。就是典型的“發(fā)布訂閱者”模式漓帚,數(shù)據(jù)變化為“發(fā)布者”,依賴對象為“訂閱者”民傻。
Proxy 與Reflect 組合使用胰默,Proxy攔截用戶對目標(biāo)對象的訪問场斑, 而實(shí)際對數(shù)據(jù)的操作由Reflect來完成
Reflect
Reflect對象與Proxy對象一樣,是ES6為了操作對象而提供的新API 牵署,Reflect不能執(zhí)行new指令漏隐。
Reflect作用:優(yōu)化Object的一些操作方法以及合理的返回Object操作返回的結(jié)果。
const baseHandler = {
get(target, key) {
// Reflect.get
const res = Reflect.get(target, key);
// @todo 依賴收集
// 嘗試獲取值obj.age奴迅,觸發(fā)getter
track(target, key);
return typeof res === "object" ? reactive(res) : res;
},
set(target, key, val) {
const info = { oldValue: target[key], newValue: val };
// Reflect.set
// target[key] = val;
const res = Reflect.set(target, key, val);
// @todo 響應(yīng)式去通知變化 觸發(fā)執(zhí)行青责,effect函數(shù)是響應(yīng)式對象修改觸發(fā)的
trigger(target, key, info);
},
};
track()
函數(shù)用來收集依賴,將所有 get 的 target 跟 key 以及 effect 建立起對應(yīng)關(guān)系取具,使用一個(gè)全局的 WeakMap 類型變量 targetMap 來存儲 target脖隶,還需要一個(gè)全局的數(shù)組來存儲 effect
effect是副作用的意思,也就是說它是響應(yīng)式副產(chǎn)品暇检,每次觸發(fā)了 get 時(shí)收集effect产阱,每次set時(shí)在觸發(fā)這些effects,這樣就可以做一些響應(yīng)式數(shù)據(jù)之外的一些事情了块仆,比如計(jì)算屬性computed
function track(target, key) {
const effect = effectStack[effectStack.length - 1];
if (effect) {
let depMap = targetMap.get(target);
if (depMap === undefined) {
depMap = new Map();
targetMap.set(target, depMap);
}
let dep = depMap.get(key);
if (dep === undefined) {
dep = new Set(); // key去重
depMap.set(key, dep);
}
// 以上為容錯(cuò) target key
if (!dep.has(effect)) {
// 新增依賴
// 雙向存儲构蹬,方便查找優(yōu)化
dep.add(effect);
effect.deps.push(dep);
}
}
}
trigger()
函數(shù)用來通知訂閱者,更新數(shù)據(jù)悔据,執(zhí)行effect庄敛,普通的effect和computed有優(yōu)先級,effect先執(zhí)行科汗,computed后執(zhí)行藻烤,因?yàn)?computed 可能會(huì)依賴普通的 effect
function trigger(target, key, info) {
//1.找到依賴
const depMap = targetMap.get(target);
if (depMap === undefined) {
// 沒有依賴直接return
return;
}
// 區(qū)分普通的effect和computed有優(yōu)先級,effect先執(zhí)行头滔,computed后執(zhí)行
// 因?yàn)?computed 可能會(huì)依賴普通的 effect
const effects = new Set();
const computedRunners = new Set();
if (key) {
let deps = depMap.get(key);
deps.forEach((effect) => {
if (effect.computed) {
computedRunners.add(effect);
} else {
effects.add(effect);
}
});
// 拆開執(zhí)行
effects.forEach((effect) => effect());
computedRunners.forEach((computed) => computed());
}
}
Demo的github地址:https://github.com/lihel/proxy-demo
注:
proxy的兼容性不是很好怖亭,由于ES5的限制,ES6新增的Proxy無法被轉(zhuǎn)譯成ES5坤检,目前可以通過Polyfill提供部分兼容
https://www.npmjs.com/package/proxy-polyfill