美團(tuán)技術(shù)面:你可以手寫Vue3的響應(yīng)式原理嗎?
在上一篇嗶哩嗶哩面試官:你可以手寫Vue2的響應(yīng)式原理嗎?中,我們已經(jīng)了解了Vue2中的響應(yīng)式原理并且動手實(shí)現(xiàn)了其核心邏輯。但是Vue2的響應(yīng)式原理是存在一些缺點(diǎn)的:
默認(rèn)會遞歸、消耗較大 數(shù)組響應(yīng)化需要額外實(shí)現(xiàn) 新增/刪除屬性屬性無法監(jiān)聽 Map、Set、Class 等無法響應(yīng)式,修改語法有限制
而Vue3使用ES6的Proxy特性來解決上面這些問題,本篇文章我將帶大家深入了解Vue3的響應(yīng)式原理并在最后通過Proxy實(shí)現(xiàn)其核心邏輯。
在開始分析之前,我們先來看一下什么是 Proxy?
什么是 Proxy?
ES6 中我們看到了一個讓人耳目一新的屬性——Proxy。我們先看一下概念:
“通過調(diào)用
new Proxy(),你可以創(chuàng)建一個代理用來替代另一個對象(被稱為目標(biāo)),這個代理對目標(biāo)對象進(jìn)行了虛擬,因此該代理與該目標(biāo)對象表面上可以被當(dāng)作同一個對象來對待。代理允許你攔截在目標(biāo)對象上的底層操作,而這原本是JS引擎的內(nèi)部能力。
Proxy 顧名思義,就是代理的意思,這是一個能讓我們隨意操控對象的特性。當(dāng)我們通過 Proxy 去對一個對象進(jìn)行代理之后,我們將得到一個和被代理對象幾乎完全一樣的對象,并且可以對這個對象進(jìn)行完全的監(jiān)控。
什么叫完全監(jiān)控?Proxy 所帶來的,是對底層操作的攔截。前面我們在實(shí)現(xiàn)對對象監(jiān)聽時使用了 Object.defineProperty,這個其實(shí)是 JS 提供給我們的高級操作,也就是通過底層封裝之后暴露出來的方法。Proxy 的強(qiáng)大之處在于,我們可以直接攔截對代理對象的底層操作。這樣我們相當(dāng)于從一個對象的底層操作開始實(shí)現(xiàn)對它的監(jiān)聽。
那么Proxy相比Object.defineProperty都有哪些優(yōu)勢呢?
Proxy 的優(yōu)勢
Proxy可以直接監(jiān)聽對象而非屬性;Proxy可以直接監(jiān)聽數(shù)組的變化;Proxy有多達(dá) 13 種攔截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具備的;Proxy返回的是一個新對象,我們可以只操作新的對象達(dá)到目的,而Object.defineProperty只能遍歷對象屬性直接修改;Proxy作為新標(biāo)準(zhǔn)將受到瀏覽器廠商重點(diǎn)持續(xù)的性能優(yōu)化,也就是傳說中的新標(biāo)準(zhǔn)的性能紅利。
對Proxy有了大致的了解后,下面我就來分析一下Vue3的響應(yīng)式原理
響應(yīng)式原理
這里放一張我之前整理的關(guān)于Vue3響應(yīng)式的流程圖:

我們來梳理一下流程:
1、通過state = reactive(target)來定義響應(yīng)式數(shù)據(jù)(這里基于Proxy實(shí)現(xiàn))
2、通過 effect聲明依賴響應(yīng)式數(shù)據(jù)的函數(shù)cb ( 例如視圖渲染函數(shù)render函數(shù)),并執(zhí)行cb函數(shù),執(zhí)行過程中,會觸發(fā)響應(yīng)式數(shù)據(jù) getter
3、在響應(yīng)式數(shù)據(jù) getter中進(jìn)行 track依賴收集:存儲響應(yīng)式數(shù)據(jù)與更新函數(shù) cb 的映射關(guān)系,存儲于targetMap
4、當(dāng)變更響應(yīng)式數(shù)據(jù)時,觸發(fā)trigger,根據(jù)targetMap找到關(guān)聯(lián)的cb并執(zhí)行
“
targetMap的結(jié)構(gòu)為:{target: {key: [fn1,fn2]}}
手寫實(shí)現(xiàn)
看下我們都要實(shí)現(xiàn)哪些核心函數(shù):
reactive:響應(yīng)式核心方法,用于建立數(shù)據(jù)響應(yīng)式effect:聲明響應(yīng)函數(shù) cb,將回調(diào)函數(shù)保存起來備用,立即執(zhí)行一次回調(diào)函數(shù)觸發(fā)它里面一些響應(yīng)數(shù)據(jù)的 gettertrack:依賴收集,存儲響應(yīng)式數(shù)據(jù)與更新函數(shù) cb 的映射關(guān)系trigger:觸發(fā)更新:根據(jù)映射關(guān)系,執(zhí)行 cb
建立數(shù)據(jù)響應(yīng)式(reactive函數(shù))
// 判斷是不是對象
function isObject(val) {
return typeof val === "object" && val !== null;
}
function hasOwn(target, key) {
return target.hasOwnProperty[key];
}
// WeakMap: 弱引用映射表
// 原對象 : 代理過的對象
let toProxy = new WeakMap();
// 代理過的對象:原對象
let toRaw = new WeakMap();
// 響應(yīng)式核心方法
function reactive(target) {
// 創(chuàng)建響應(yīng)式對象
return createReactiveObject(target);
}
function createReactiveObject(target) {
// 如果當(dāng)前不是對象,直接返回即可
if (!isObject(target)) {
return target;
}
// 如果已經(jīng)代理過了,就直接返回代理過的結(jié)果
let proxy = toProxy.get(target);
if (proxy) {
return proxy;
}
// 防止代理過的對象再次被代理
if (toRaw.has(target)) {
return target;
}
let baseHandler = {
get(target, key, receiver) {
// Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
let res = Reflect.get(target, key, receiver);
// 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
track(target, key);
// 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
// 這里需要區(qū)分是新增屬性還是修改屬性
let hasKey = hasOwn(target, key);
let oldVal = target[key];
let res = Reflect.set(target, key, value, receiver);
if (!hasKey) {
console.log("新增屬性");
trigger(target, "add", key);
} else if (oldVal !== value) {
console.log("修改屬性");
trigger(target, "set", key);
}
return res;
},
deleteProperty(target, key) {
let res = Reflect.deleteProperty(target, key);
return res;
},
};
let observed = new Proxy(target, baseHandler);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
依賴收集
其實(shí)就是建立響應(yīng)數(shù)據(jù) key 和更新函數(shù)之間的對應(yīng)關(guān)系,用法如下:
let obj = reactive({ name: "cosen" });
effect(() => {
console.log(obj.name);
});
obj.name = "senlin";
obj.name = "senlin1";
要實(shí)現(xiàn)這部分功能,我們需要完成上面提到的三個方法:
effecttracktrigger
首先,我們來梳理一下effect需要實(shí)現(xiàn)什么功能。
經(jīng)過前面的reactive()方法,我們已經(jīng)能夠拿到一個響應(yīng)式的數(shù)據(jù)對象了,每次get和set操作都能夠被攔截。
effect()方法需要實(shí)現(xiàn)的功能就是:每當(dāng)我們修改數(shù)據(jù)的時候,都能夠觸發(fā)傳入effect的回調(diào)函數(shù)執(zhí)行。
“
effect()方法的回調(diào)函數(shù)要想在數(shù)據(jù)發(fā)生變化后能夠執(zhí)行,必須返回一個響應(yīng)式的effect()函數(shù),所以effect()內(nèi)部會返回一個響應(yīng)式的effect。
來看下effect方法的實(shí)現(xiàn):
// 響應(yīng)式 副作用
function effect(fn) {
const rxEffect = function () {
try {
// 捕獲異常
// 運(yùn)行fn并將effect保存起來
activeEffectStacks.push(rxEffect);
return fn();
} finally {
activeEffectStacks.pop();
}
};
// 默認(rèn)應(yīng)該先執(zhí)行一次
rxEffect();
// 返回響應(yīng)函數(shù)
return rxEffect;
}
此時數(shù)據(jù)發(fā)生變化還無法通知effect的回調(diào)函數(shù)執(zhí)行,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(71, 193, 168);">reactive和effect還未關(guān)聯(lián)起來,也就是說還沒有進(jìn)行依賴收集,所以接下來需要進(jìn)行依賴收集。
到這里我們需要思考兩個問題:
1、什么時候收集依賴?
2、如何收集依賴,如何保存依賴?
首先第一個問題:什么時候收集依賴?我們需要在取值的時候開始收集依賴,而這對應(yīng)于在Proxy的handlers的get中進(jìn)行取值,也就是在上面的createReactiveObject方法中的:
get(target, key, receiver) {
// Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
let res = Reflect.get(target, key, receiver);
// 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
+ track(target, key);
// 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
return isObject(res) ? reactive(res) : res;
},
對應(yīng)觸發(fā)依賴的執(zhí)行是在Proxy的handlers的get中:
set(target, key, value, receiver) {
// 這里需要區(qū)分是新增屬性還是修改屬性
let hasKey = hasOwn(target, key);
let oldVal = target[key];
let res = Reflect.set(target, key, value, receiver);
if (!hasKey) {
console.log("新增屬性");
+ trigger(target, "add", key);
} else if (oldVal !== value) {
console.log("修改屬性");
+ trigger(target, "set", key);
}
return res;
},
然后是第二個問題:如何收集依賴,如何保存依賴?這個其實(shí)我有在上面的流程圖中標(biāo)注:
{target: {key: [fn1,fn2]}}
這里解釋一下:首先依賴是一個一個的effect函數(shù),我們可以通過Set集合進(jìn)行存儲,而這個 Set 集合肯定是要和對象的某個key進(jìn)行對應(yīng),即哪些effect依賴了對象中某個key對應(yīng)的值,這個對應(yīng)關(guān)系可以通過一個Map對象進(jìn)行保存。即:
targetMap: WeakMap{
target:Map{
key: Set[cb1,cb2...]
}
}
當(dāng)我們?nèi)≈档臅r候,首先通過該target對象從全局的WeakMap對象中取出對應(yīng)的depsMap對象,然后根據(jù)修改的key獲取到對應(yīng)的dep依賴集合對象,然后將當(dāng)前effect放入到dep依賴集合中,完成依賴的收集。其實(shí)這里對應(yīng)的就是track方法:
function track(target, key) {
// 拿出棧頂函數(shù)
let effect = activeEffectStacks[activeEffectStacks.length - 1];
//
if (effect) {
// 獲取target對應(yīng)依賴表
let depsMap = targetsMap.get(target);
if (!depsMap) {
targetsMap.set(target, (depsMap = new Map()));
}
// 獲取key對應(yīng)的響應(yīng)函數(shù)集
let deps = depsMap.get(key);
// 動態(tài)創(chuàng)建依賴關(guān)系
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
當(dāng)我們修改值的時候會觸發(fā)依賴更新,也是通過target對象從全局的WeakMap對象中取出對應(yīng)的depMap對象,然后根據(jù)修改的key取出對應(yīng)的dep依賴集合,并遍歷該集合中的所有effect,并執(zhí)行effect。對應(yīng)就是trigger方法:
function trigger(target, type, key) {
let depsMap = targetsMap.get(target);
if (depsMap) {
let deps = depsMap.get(key);
if (deps) {
// 將當(dāng)前key對應(yīng)的effect依次執(zhí)行
deps.forEach((effect) => {
effect();
});
}
}
}
完整代碼
這里整合一下代碼,并在最后通過一個demo來測試一下:
/**
* Vue3 響應(yīng)式原理
*
*/
// 判斷是不是對象
function isObject(val) {
return typeof val === "object" && val !== null;
}
function hasOwn(target, key) {
return target.hasOwnProperty[key];
}
// WeakMap: 弱引用映射表
// 原對象 : 代理過的對象
let toProxy = new WeakMap();
// 代理過的對象:原對象
let toRaw = new WeakMap();
// 響應(yīng)式核心方法
function reactive(target) {
// 創(chuàng)建響應(yīng)式對象
return createReactiveObject(target);
}
function createReactiveObject(target) {
// 如果當(dāng)前不是對象,直接返回即可
if (!isObject(target)) {
return target;
}
// 如果已經(jīng)代理過了,就直接返回代理過的結(jié)果
let proxy = toProxy.get(target);
if (proxy) {
return proxy;
}
// 防止代理過的對象再次被代理
if (toRaw.has(target)) {
return target;
}
let baseHandler = {
get(target, key, receiver) {
// Reflect 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
let res = Reflect.get(target, key, receiver);
// 收集依賴/訂閱 把當(dāng)前的key和effect做映射關(guān)系
track(target, key);
// 在get取值的時候才去判斷該值是否是一個對象,如果是則遞歸(這里相比于Vue2中的默認(rèn)遞歸,其實(shí)是一種優(yōu)化)
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
// 這里需要區(qū)分是新增屬性還是修改屬性
let hasKey = hasOwn(target, key);
let oldVal = target[key];
let res = Reflect.set(target, key, value, receiver);
if (!hasKey) {
console.log("新增屬性");
trigger(target, "add", key);
} else if (oldVal !== value) {
console.log("修改屬性");
trigger(target, "set", key);
}
return res;
},
deleteProperty(target, key) {
let res = Reflect.deleteProperty(target, key);
return res;
},
};
let observed = new Proxy(target, baseHandler);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
// 棧 先進(jìn)后出 {name:[effect]}
let activeEffectStacks = [];
let targetsMap = new WeakMap();
// 如果target中的key發(fā)生變化了,就執(zhí)行數(shù)組里的方法
function track(target, key) {
// 拿出棧頂函數(shù)
let effect = activeEffectStacks[activeEffectStacks.length - 1];
if (effect) {
// 獲取target對應(yīng)依賴表
let depsMap = targetsMap.get(target);
if (!depsMap) {
targetsMap.set(target, (depsMap = new Map()));
}
// 獲取key對應(yīng)的響應(yīng)函數(shù)集
let deps = depsMap.get(key);
// 動態(tài)創(chuàng)建依賴關(guān)系
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
function trigger(target, type, key) {
let depsMap = targetsMap.get(target);
if (depsMap) {
let deps = depsMap.get(key);
if (deps) {
// 將當(dāng)前key對應(yīng)的effect依次執(zhí)行
deps.forEach((effect) => {
effect();
});
}
}
}
// 響應(yīng)式 副作用
function effect(fn) {
const rxEffect = function () {
try {
// 捕獲異常
// 運(yùn)行fn并將effect保存起來
activeEffectStacks.push(rxEffect);
return fn();
} finally {
activeEffectStacks.pop();
}
};
// 默認(rèn)應(yīng)該先執(zhí)行一次
rxEffect();
// 返回響應(yīng)函數(shù)
return rxEffect;
}
let obj = reactive({ name: "cosen" });
effect(() => {
console.log(obj.name);
});
obj.name = "senlin";
obj.name = "senlin";
順便貼下運(yùn)行的結(jié)果:
我們能看到雖然執(zhí)行了兩次的obj.name = "senlin"操作,但執(zhí)行結(jié)果卻只執(zhí)行了一次,這個與代碼中定義的toProxy和toRaw是有關(guān)的:
toProxy:存儲原對象到代理過的對象的映射關(guān)系,如果已經(jīng)代理過了,就直接返回代理過的結(jié)果toRaw存儲代理過的對到原對象的映射關(guān)系,防止代理過的對象再次被代理。
總結(jié)
ok,到這里,我基本把Vue3中關(guān)于響應(yīng)式以及依賴收集的相關(guān)原理和大家梳理了一遍,也自己手動實(shí)現(xiàn)了一個簡易的偽代碼。
本文只是簡單的用偽代碼的形式做了演示,關(guān)于具體實(shí)現(xiàn)細(xì)節(jié),如果你想更深入的了解,大家可以直接去查看Vue3響應(yīng)式部分的源碼[1]。
參考資料
Vue3響應(yīng)式部分的源碼: https://github.com/vuejs/vue-next/tree/master/packages/reactivity
