
vue3.0監(jiān)測(cè)機(jī)制有了很大的改善,彌補(bǔ)了vue2.0
的一些局限:
- 對(duì)屬性的添加、刪除動(dòng)作的監(jiān)測(cè);
- 對(duì)數(shù)組基于下標(biāo)的修改、對(duì)于 .length 修改的監(jiān)測(cè);
- 對(duì) Map、Set、WeakMap 和 WeakSet 的支持;
這里為了更好的理解原理手動(dòng)實(shí)現(xiàn)vue3.0的監(jiān)測(cè)對(duì)象屬性變化的Demo(實(shí)際源碼中還需考慮map, set等數(shù)據(jù)類型,這里僅用普通對(duì)象為例)
vue3.0 使用proxy代替了vue2.0版本中的defineProperty,首先利用compositionAPI中的 reactive() 函數(shù)返回一個(gè)proxy對(duì)象,使得數(shù)據(jù)可監(jiān)測(cè)
// reactive() 函數(shù)接受一個(gè)普通對(duì)象 返回一個(gè)響應(yīng)式數(shù)據(jù)對(duì)象
function reactive(target) {
// 通過(guò)proxy將對(duì)象變?yōu)轫憫?yīng)式
const observed = new Proxy(target, baseHandler);
// 返回proxy代理后的對(duì)象
return observed;
}
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫(xiě)
var proxy = new Proxy(target, handler);
target參數(shù)表示所要攔截的目標(biāo)對(duì)象,handler參數(shù)也是一個(gè)對(duì)象,用來(lái)定制攔截行為。
baseHandler中定義攔截的get set方法,監(jiān)測(cè)和改寫(xiě)數(shù)據(jù),為了方便,我們需要先將所有依賴收集起來(lái),一旦數(shù)據(jù)發(fā)生變化,就統(tǒng)一通知更新。就是典型的“發(fā)布訂閱者”模式,數(shù)據(jù)變化為“發(fā)布者”,依賴對(duì)象為“訂閱者”。
Proxy 與Reflect 組合使用,Proxy攔截用戶對(duì)目標(biāo)對(duì)象的訪問(wèn), 而實(shí)際對(duì)數(shù)據(jù)的操作由Reflect來(lái)完成
Reflect
Reflect對(duì)象與Proxy對(duì)象一樣,是ES6為了操作對(duì)象而提供的新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)式對(duì)象修改觸發(fā)的
trigger(target, key, info);
},
};
track() 函數(shù)用來(lái)收集依賴,將所有 get 的 target 跟 key 以及 effect 建立起對(duì)應(yīng)關(guān)系,使用一個(gè)全局的 WeakMap 類型變量 targetMap 來(lái)存儲(chǔ) target,還需要一個(gè)全局的數(shù)組來(lái)存儲(chǔ) effect
effect是副作用的意思,也就是說(shuō)它是響應(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)) {
// 新增依賴
// 雙向存儲(chǔ),方便查找優(yōu)化
dep.add(effect);
effect.deps.push(dep);
}
}
}
trigger() 函數(shù)用來(lái)通知訂閱者,更新數(shù)據(jù),執(zhí)行effect,普通的effect和computed有優(yōu)先級(jí),effect先執(zhí)行,computed后執(zhí)行,因?yàn)?computed 可能會(huì)依賴普通的 effect
function trigger(target, key, info) {
//1.找到依賴
const depMap = targetMap.get(target);
if (depMap === undefined) {
// 沒(méi)有依賴直接return
return;
}
// 區(qū)分普通的effect和computed有優(yōu)先級(jí),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);
}
});
// 拆開(kāi)執(zhí)行
effects.forEach((effect) => effect());
computedRunners.forEach((computed) => computed());
}
}
Demo的github地址:https://github.com/lihel/proxy-demo
注:
proxy的兼容性不是很好,由于ES5的限制,ES6新增的Proxy無(wú)法被轉(zhuǎn)譯成ES5,目前可以通過(guò)Polyfill提供部分兼容
https://www.npmjs.com/package/proxy-polyfill