新聞中心
而且不只是 Vue,狀態(tài)管理庫 Mobx 也是基于響應(yīng)式實(shí)現(xiàn)的。

那響應(yīng)式是具體怎么實(shí)現(xiàn)的呢?
與其空談原理,不如讓我們來手寫一個(gè)簡(jiǎn)易版吧。
響應(yīng)式
首先,什么是響應(yīng)式呢?
響應(yīng)式就是被觀察的數(shù)據(jù)變化的時(shí)候做一系列聯(lián)動(dòng)處理。
就像一個(gè)社會(huì)熱點(diǎn)事件,當(dāng)它有消息更新的時(shí)候,各方媒體都會(huì)跟進(jìn)做相關(guān)報(bào)道。
這里社會(huì)熱點(diǎn)事件就是被觀察的目標(biāo)。
那在前端框架里,這個(gè)被觀察的目標(biāo)是什么呢?
很明顯,是狀態(tài)。
狀態(tài)一般是多個(gè),會(huì)通過對(duì)象的方式來組織。所以,我們觀察狀態(tài)對(duì)象的每個(gè) key 的變化,聯(lián)動(dòng)做一系列處理就可以了。
我們要維護(hù)這樣的數(shù)據(jù)結(jié)構(gòu):
狀態(tài)對(duì)象的每個(gè) key 都有關(guān)聯(lián)的一系列 effect 副作用函數(shù),也就是變化的時(shí)候聯(lián)動(dòng)執(zhí)行的邏輯,通過 Set 來組織。
每個(gè) key 都是這樣關(guān)聯(lián)了一系列 effect 函數(shù),那多個(gè) key 就可以放到一個(gè) Map 里維護(hù)。
這個(gè) Map 是在對(duì)象存在的時(shí)候它就存在,對(duì)象銷毀的時(shí)候它也要跟著銷毀。(因?yàn)閷?duì)象都沒了自然也不需要維護(hù)每個(gè) key 關(guān)聯(lián)的 effect 了)
而 WeakMap 正好就有這樣的特性,WeakMap 的 key 必須是一個(gè)對(duì)象,value 可以是任意數(shù)據(jù),key 的對(duì)象銷毀的時(shí)候,value 也會(huì)銷毀。
所以,響應(yīng)式的 Map 會(huì)用 WeakMap 來保存,key 為原對(duì)象。
這個(gè)數(shù)據(jù)結(jié)構(gòu)就是響應(yīng)式的核心數(shù)據(jù)結(jié)構(gòu)了。
比如這樣的狀態(tài)對(duì)象:
const obj = {
a: 1,
b: 2
}它的響應(yīng)式數(shù)據(jù)結(jié)構(gòu)就是這樣的:
const depsMap = new Map();
const aDeps = new Set();
depsMap.set('a', aDeps);
const bDeps = new Set();
depsMap.set('b', bDeps);
const reactiveMap = new WeakMap()
reactiveMap.set(obj, depsMap);
創(chuàng)建出的數(shù)據(jù)結(jié)構(gòu)就是圖中的那個(gè):
然后添加 deps 依賴,比如一個(gè)函數(shù)依賴了 a,那就要添加到 a 的 deps 集合里:
effect(() => {
console.log(obj.a);
});也就是這樣:
const depsMap = reactiveMap.get(obj);
const aDeps = depsMap.get('a');
aDeps.add(該函數(shù));
=這樣維護(hù) deps 功能上沒啥問題,但是難道要讓用戶手動(dòng)添加 deps 么?
那不但會(huì)侵入業(yè)務(wù)代碼,而且還容易遺漏。
所以肯定不會(huì)讓用戶手動(dòng)維護(hù) deps,而是要做自動(dòng)的依賴收集。
那怎么自動(dòng)收集依賴呢?
讀取狀態(tài)值的時(shí)候,就建立了和該狀態(tài)的依賴關(guān)系,所以很容易想到可以代理狀態(tài)的 get 來實(shí)現(xiàn)。
通過 Object.defineProperty 或者 Proxy 都可以:
const data = {
a: 1,
b: 2
}
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
get(targetObj, key) {
let depsMap = reactiveMap.get(targetObj);
if (!depsMap) {
reactiveMap.set(targetObj, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
return targetObj[key]
}
})effect 會(huì)執(zhí)行傳入的回調(diào)函數(shù) fn,當(dāng)你在 fn 里讀取 obj.a 的時(shí)候,就會(huì)觸發(fā) get,會(huì)拿到對(duì)象的響應(yīng)式的 Map,從里面取出 a 對(duì)應(yīng)的 deps 集合,往里面添加當(dāng)前的 effect 函數(shù)。
這樣就完成了一次依賴收集。
當(dāng)你修改 obj.a 的時(shí)候,要通知所有的 deps,所以還要代理 set:
set(targetObj, key, newVal) {
targetObj[key] = newVal
const depsMap = reactiveMap.get(targetObj)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}基本的響應(yīng)式完成了,我們測(cè)試一下:
打印了兩次,第一次是 1,第二次是 3。
effect 會(huì)先執(zhí)行一次傳入的回調(diào)函數(shù),觸發(fā) get 來收集依賴,這時(shí)候打印的 obj.a 是 1
然后當(dāng) obj.a 賦值為 3 后,會(huì)觸發(fā) set,執(zhí)行收集的依賴,這時(shí)候打印 obj.a 是 3
依賴也正確收集到了:
結(jié)果是對(duì)的,我們完成了基本的響應(yīng)式!
當(dāng)然,響應(yīng)式不會(huì)只有這么點(diǎn)代碼的,我們現(xiàn)在的實(shí)現(xiàn)還不完善,還有一些問題。
比如,如果代碼里有分支切換,上次執(zhí)行會(huì)依賴 obj.b 下次執(zhí)行又不依賴了,這時(shí)候是不是就有了無效的依賴?
這樣一段代碼:
const obj = {
a: 1,
b: 2
}
effect(() => {
console.log(obj.a ? obj.b : 'nothing');
});
obj.a = undefined;
obj.b = 3;第一次執(zhí)行 effect 函數(shù),obj.a 是 1,這時(shí)候會(huì)走到第一個(gè)分支,又依賴了 obj.b。
把 obj.a 修改為 undefined,觸發(fā) set,執(zhí)行所有的依賴函數(shù),這時(shí)候走到分支二,不再依賴 obj.b。
把 obj.b 修改為 3,按理說這時(shí)候沒有依賴 b 的函數(shù)了,我們執(zhí)行試一下:
第一次打印 2 是對(duì)的,也就是走到了第一個(gè)分支,打印 obj.b
第二次打印 nothing 也是對(duì)的,這時(shí)候走到第二個(gè)分支。
但是第三次打印 nothing 就不對(duì)了,因?yàn)檫@時(shí)候 obj.b 已經(jīng)沒有依賴函數(shù)了,但是還是打印了。
打印看下 deps,會(huì)發(fā)現(xiàn) obj.b 的 deps 沒有清除
所以解決方案就是每次添加依賴前清空下上次的 deps。
怎么清空某個(gè)函數(shù)關(guān)聯(lián)的所有 deps 呢?
記錄下就好了。
我們改造下現(xiàn)有的 effect 函數(shù):
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
記錄下這個(gè) effect 函數(shù)被放到了哪些 deps 集合里。也就是:
let activeEffect
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
對(duì)之前的 fn 包一層,在函數(shù)上添加個(gè) deps 數(shù)組來記錄被添加到哪些依賴集合里。
get 收集依賴的時(shí)候,也記錄一份到這里:
這樣下次再執(zhí)行這個(gè) effect 函數(shù)的時(shí)候,就可以把這個(gè) effect 函數(shù)從上次添加到的依賴集合里刪掉:
cleanup 實(shí)現(xiàn)如下:
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}effectFn.deps 數(shù)組記錄了被添加到的 deps 集合,從中刪掉自己。
全刪完之后就把上次記錄的 deps 數(shù)組置空。
我們?cè)賮頊y(cè)試下:
無限循環(huán)打印了,什么鬼?
問題出現(xiàn)在這里:
set 的時(shí)候會(huì)執(zhí)行所有的當(dāng)前 key 的 deps 集合里的 effect 函數(shù)。
而我們執(zhí)行 effect 函數(shù)之前會(huì)把它從之前的 deps 集合中清掉:
執(zhí)行的時(shí)候又被添加到了 deps 集合。
這樣 delete 又 add,delete 又 add,所以就無限循環(huán)了。
解決的方式就是創(chuàng)建第二個(gè) Set,只用于遍歷:
這樣就不會(huì)無限循環(huán)了。
再測(cè)試一次:
現(xiàn)在當(dāng) obj.a 賦值為 undefined 之后,再次執(zhí)行 effect 函數(shù),obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不會(huì)打印啥。
看下現(xiàn)在的響應(yīng)式數(shù)據(jù)結(jié)構(gòu):
確實(shí),b 的 deps 集合被清空了。
那現(xiàn)在的響應(yīng)式實(shí)現(xiàn)是完善的了么?
也不是,還有一個(gè)問題:
如果 effect 嵌套了,那依賴還能正確的收集么?
首先講下為什么要支持 effect 嵌套,因?yàn)榻M件是可以嵌套的,而且組件里會(huì)寫 effect,那也就是 effect 嵌套了,所以必須支持嵌套。
我們嵌套下試試:
effect(() => {
console.log('effect1');
effect(() => {
console.log('effect2');
obj.b;
});
obj.a;
});
obj.a = 3;按理說會(huì)打印一次 effect1、一次 effect2,這是最開始的那次執(zhí)行。
然后 obj.a 修改為 3 后,會(huì)觸發(fā)一次 effect1 的打印,執(zhí)行內(nèi)層 effect,又觸發(fā)一次 effect2 的打印。
也就是會(huì)打印 effect1、effect2、effect1、effect2。
我們測(cè)試下:
打印了 effect1、effet2 這是對(duì)的,但第三次打印的是 effect2,這說明 obj.a 修改后并沒有執(zhí)行外層函數(shù),而是執(zhí)行的內(nèi)層函數(shù)。
為什么呢?
看下這段代碼:
我們執(zhí)行 effect 的時(shí)候,會(huì)把它賦值給一個(gè)全局變量 activeEffect,然后后面收集依賴就用的這個(gè)。
當(dāng)嵌套 effect 的時(shí)候,內(nèi)層函數(shù)執(zhí)行后會(huì)修改 activeEffect 這樣收集到的依賴就不對(duì)了。
怎么辦呢?
嵌套的話加一個(gè)棧來記錄 effect 不就行了?
也就是這樣:
執(zhí)行 effect 函數(shù)前把當(dāng)前 effectFn 入棧,執(zhí)行完以后出棧,修改 activeEffect 為棧頂?shù)?effectFn。
這樣就保證了收集到的依賴是正確的。
這種思想的應(yīng)用還是很多的,需要保存和恢復(fù)上下文的時(shí)候,都是這樣加一個(gè)棧。
我們?cè)贉y(cè)試一下:
現(xiàn)在的打印就對(duì)了。
至此,我們的響應(yīng)式系統(tǒng)就算比較完善了。
全部代碼如下:
const data = {
a: 1,
b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
get(targetObj, key) {
let depsMap = reactiveMap.get(targetObj)
if (!depsMap) {
reactiveMap.set(targetObj, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps);
return targetObj[key]
},
set(targetObj, key, newVal) {
targetObj[key] = newVal
const depsMap = reactiveMap.get(targetObj)
if (!depsMap) return
const effects = depsMap.get(key)
// effects && effects.forEach(fn => fn())
const effectsToRun = new Set(effects);
effectsToRun.forEach(effectFn => effectFn());
}
})總結(jié)
響應(yīng)式就是數(shù)據(jù)變化的時(shí)候做一系列聯(lián)動(dòng)的處理。
核心是這樣一個(gè)數(shù)據(jù)結(jié)構(gòu):
最外層是 WeakMap,key 為對(duì)象,value 為響應(yīng)式的 Map。這樣當(dāng)對(duì)象銷毀時(shí),Map 也會(huì)銷毀。
Map 里保存了每個(gè) key 的依賴集合,用 Set 組織。
我們通過 Proxy 來完成自動(dòng)的依賴收集,也就是添加 effect 到對(duì)應(yīng) key 的 deps 的集合里。set 的時(shí)候觸發(fā)所有的 effect 函數(shù)執(zhí)行。
這就是基本的響應(yīng)式系統(tǒng)。
但是還不夠完善,每次執(zhí)行 effect 前要從上次添加到的 deps 集合中刪掉它,然后重新收集依賴。這樣可以避免因?yàn)榉种袚Q產(chǎn)生的無效依賴。
并且執(zhí)行 deps 中的 effect 前要?jiǎng)?chuàng)建一個(gè)新的 Set 來執(zhí)行,避免 add、delete 循環(huán)起來。
此外,為了支持嵌套 effect,需要在執(zhí)行 effect 之前把它推到棧里,然后執(zhí)行完出棧。
解決了這幾個(gè)問題之后,就是一個(gè)完善的 Vue 響應(yīng)式系統(tǒng)了。
當(dāng)然,現(xiàn)在雖然功能是完善的,但是沒有實(shí)現(xiàn) computed、watch 等功能,之后再實(shí)現(xiàn)。
最后,再來看一下這個(gè)數(shù)據(jù)結(jié)構(gòu),理解了它就理解了 vue 響應(yīng)式的核心:
本文標(biāo)題:手寫Vue3響應(yīng)式系統(tǒng):核心就一個(gè)數(shù)據(jù)結(jié)構(gòu)
瀏覽地址:http://www.dlmjj.cn/article/dpisohi.html


咨詢
建站咨詢
