您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么用代碼實(shí)現(xiàn)一個(gè)迷你響應(yīng)式系統(tǒng)vue”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“怎么用代碼實(shí)現(xiàn)一個(gè)迷你響應(yīng)式系統(tǒng)vue”吧!
什么是響應(yīng)式系統(tǒng)?學(xué)術(shù)上的定義,我們就不細(xì)究了。通過(guò)縱觀前端業(yè)界對(duì)響應(yīng)系統(tǒng)的實(shí)現(xiàn),其實(shí),這個(gè)定義是很簡(jiǎn)單的。 無(wú)非是 - 一個(gè)系統(tǒng),它能夠?qū)尤脒@個(gè)系統(tǒng)的 js 值的變化自動(dòng)地做出反應(yīng)的話,那么這個(gè)系統(tǒng)就可以稱之為「響應(yīng)式系統(tǒng)」。
從上面的基本定義來(lái)看,響應(yīng)式系統(tǒng)就包含兩個(gè)基本的,必不可少的要素:
被觀察的值
能夠響應(yīng)值發(fā)生變化的能力
「能被觀察的值」在不同的 UI 庫(kù)中叫法不一樣。比如:
mobx 中稱之為「observables」
solidjs 稱之為「signal」
vue 稱之為「ref」
recoil 稱之為 「atom」
還有稱之為「subjects」或者「state」
不管你怎么叫,它終究還是一個(gè)能被觀察的 「js 值」。顯然, 原始的 js 值是沒(méi)有響應(yīng)性的,這里的「能被觀察」正是需要我們自己去封裝實(shí)現(xiàn)的。這里的實(shí)現(xiàn)的基本思路就是「包裹」。展開(kāi)說(shuō),就是你想某個(gè) js 值能被觀察,那么它就必須被「某個(gè)東西」包裹住,然后與之配合,用戶消費(fèi)的是包裹后的產(chǎn)物而不是原始值。
實(shí)現(xiàn)「包裹」的方式不一樣,那么最終提供給用戶的 API 的風(fēng)格就不一樣。不同風(fēng)格的 API 所帶來(lái)的 DX 不同。比如,vue3 里面,它的響應(yīng)式系統(tǒng)是基于瀏覽器的原生 API Proxy
來(lái)實(shí)現(xiàn)值的包裹的。在這中技術(shù)方案下,用戶使用原生的 js 值訪問(wèn)語(yǔ)法和賦值語(yǔ)法即可:
const proxyVal = new Proxy(originVal, { get(){}, set(){} }); // 讀值 console.log(proxyVal); // 寫(xiě)值 proxyVal = newVal;
跟 vue 不同,solidjs 自己實(shí)現(xiàn)了一套顯式的讀和寫(xiě) API:
const [val, setVal] = createSignal(originVal); // 讀值 console.log(val()); // 寫(xiě)值 setVal(newVal)
以上是第一基本要素。第二個(gè)基本要素是,我們得有響應(yīng)被觀察值發(fā)生變化的能力。這種能力主要體現(xiàn)在當(dāng)我們所消費(fèi)的 js 值發(fā)生了變化后,我們要根據(jù)特定的上下文來(lái)做出對(duì)應(yīng)的反應(yīng)。js 值被消費(fèi)的最常見(jiàn)的地方就是 js 語(yǔ)句。如果我們能讓這個(gè)語(yǔ)句重新再執(zhí)行一次,那么它就能拿到最新的值。這就是所謂的響應(yīng)式。那如果能夠讓一個(gè) js 語(yǔ)句再執(zhí)行一遍呢?答案是:“把它放在函數(shù)里面,重新調(diào)用這個(gè)函數(shù)即可”。
上面所提到的「函數(shù)」就是函數(shù)式編程概念里面的「副作用」(effect)。還是老樣子,同一個(gè)東西,不同的類庫(kù)有不同的叫法。effect 又可以稱之為:
reaction
consumer(值的消費(fèi)者)
listener(值的監(jiān)聽(tīng)者)
等等。一般而言,副作用是要被響應(yīng)式系統(tǒng)接管起來(lái)的,等到被觀察的 js 值發(fā)生變化的時(shí)候,我們?cè)偃フ{(diào)用它。從而實(shí)現(xiàn)了所謂的響應(yīng)能力。這個(gè)用于接管的 API,不同的類庫(kù)有不同的叫法:
createEffect
consume
addListener
subscribe
以上是對(duì)響應(yīng)式系統(tǒng)的最基本的兩個(gè)要素的闡述。下面,我們就從這個(gè)認(rèn)知基礎(chǔ)出發(fā),循序漸進(jìn)地用 60 行代碼去實(shí)現(xiàn)一個(gè)迷你響應(yīng)系統(tǒng)。為了提高逼格,我們沿用 solidjs 響應(yīng)式系統(tǒng)所采用的相關(guān)術(shù)語(yǔ)。
包裹 js 值的根本目的就是為了監(jiān)聽(tīng)用戶對(duì)這些值的「讀」和「寫(xiě)」的兩個(gè)動(dòng)作:
function createSignal(value) { const getter = () => { console.log('我監(jiān)聽(tīng)到讀值了') return value; }; const setter = (nextValue) => { console.log('我監(jiān)聽(tīng)到寫(xiě)值了') value = nextValue; }; return [getter, setter]; } const [count, setCount] = createSignal(0) //讀 count() // 我監(jiān)聽(tīng)到讀值了 //寫(xiě) setCount(1) // 我監(jiān)聽(tīng)到寫(xiě)值了
可以說(shuō),我們的這種 API 設(shè)計(jì)改變了用戶對(duì) js 值的讀寫(xiě)習(xí)慣,甚至可以說(shuō)有點(diǎn)強(qiáng)迫性。很多人都不習(xí)慣讀值的這種語(yǔ)法是一個(gè)函數(shù)調(diào)用。沒(méi)辦法,拿人手短,吃人嘴軟,習(xí)慣就好(不就是多敲連兩個(gè)字符嗎?哈哈)。
通過(guò)這種帶有一點(diǎn)強(qiáng)制意味的 API 設(shè)計(jì),我們能夠監(jiān)聽(tīng)到用戶對(duì)所觀察值的讀和寫(xiě)。
其實(shí),上面的短短的幾行代碼是本次要實(shí)現(xiàn)的迷你型響應(yīng)系統(tǒng)的奠基框架。因?yàn)椋O乱龅?,我們就是不斷?setter 和 getter 的函數(shù)體里面堆砌代碼,以實(shí)現(xiàn)響應(yīng)式系統(tǒng)的基本功能。
用戶對(duì) js 值的消費(fèi)一般是發(fā)生在語(yǔ)句中。為了重新執(zhí)行這些語(yǔ)句,我們需要提供一個(gè) API 給用戶來(lái)將語(yǔ)句封裝起來(lái)成為一個(gè)函數(shù),然后把這個(gè)函數(shù)當(dāng)做值存儲(chǔ)起來(lái),在未來(lái)的某個(gè)時(shí)刻由系統(tǒng)去調(diào)用這個(gè)函數(shù)。當(dāng)然,順應(yīng)「語(yǔ)句」的語(yǔ)義,我們應(yīng)該在將語(yǔ)句封裝在函數(shù)里面之后,應(yīng)該馬上執(zhí)行一次:
let effect function createSignal(value) { const subscriptions = []; const getter = () => { subscriptions.push(effect) return value; }; const setter = (nextValue) => { value = nextValue; for (const sub of subscriptions) { sub() } }; return [getter, setter]; } function createEffect(fn){ effect = fn; fn() }
至此,我們算是實(shí)現(xiàn)了響應(yīng)系統(tǒng)的基本框架:
一個(gè)可以幫助 js 值被觀察的 API
一個(gè)輔助用戶創(chuàng)建 effect 的 API
熟悉設(shè)計(jì)模式的讀者可以看出,這個(gè)框架的背后其實(shí)就是「訂閱-發(fā)布模式」 - 系統(tǒng)在用戶「讀值」的時(shí)候去做訂閱,在用戶「寫(xiě)值」的時(shí)候去通知所有的訂閱者(effect)。
上面的代碼看起來(lái)好像沒(méi)問(wèn)題。不信?我們測(cè)試一下:
代碼片段1
const [count, setCount] = createSignal(0) createEffect(()=> { console.log(`count: ${count()}`); }) // 打印一次:count: 0 setCount(1) // ?
在打問(wèn)號(hào)的地方,我們期待它是打印一次count: 1
。但是實(shí)際上它一直在打印,導(dǎo)致頁(yè)面卡死了??磥?lái),setCount(1)
導(dǎo)致了無(wú)限循環(huán)調(diào)用了。仔細(xì)分析一下,我們會(huì)發(fā)現(xiàn),導(dǎo)致無(wú)限循環(huán)調(diào)用的原因在于:setCount(1)
會(huì)導(dǎo)致系統(tǒng)遍歷subscriptions
數(shù)組,去調(diào)用每一個(gè) effect。而調(diào)用 effect()
又會(huì)產(chǎn)生一次讀值。一旦讀值,我們就會(huì)把當(dāng)前全局變量effect
push 到subscriptions
數(shù)組。這就會(huì)導(dǎo)致了我們的 subscriptions
數(shù)組永遠(yuǎn)遍歷不完。我們可以通過(guò)組合下面兩個(gè)防守來(lái)解決這個(gè)問(wèn)題:
防止同一個(gè) effect 被重復(fù) push 到 subscriptions
數(shù)組里面了。
先對(duì) subscriptions
數(shù)組做淺拷貝,再遍歷這個(gè)淺拷貝的數(shù)組。
修改后的代碼如下:
function createSignal(value) { const subscriptions = []; const getter = () => { if(!subscriptions.includes(effect)){ subscriptions.push(effect) } return value; }; const setter = (nextValue) => { value = nextValue; for (const sub of [...subscriptions]) { sub() } }; return [getter, setter]; }
我們?cè)儆蒙厦妗复a片段1」去測(cè)試一下,你會(huì)發(fā)現(xiàn),結(jié)果是符合預(yù)期的,沒(méi)有 bug。
細(xì)心的讀者可能會(huì)注意到,其實(shí)上面的代碼還是可以有優(yōu)化的空間的 - 我們可以讓它更精簡(jiǎn)和健壯。
首先我們看看這段防守代碼:
if(!subscriptions.includes(effect)){ subscriptions.push(effect) }
這段代碼的目的不言而喻,我們不希望 subscriptions
存在「重復(fù)的」effect。一提到去重相關(guān)的需求,我們得馬上想到「自帶去重功能的」,ES6 規(guī)范添加的新的數(shù)據(jù)結(jié)構(gòu) 「Set」。于是,我們用 Set 來(lái)代替數(shù)組:
function createSignal(value) { const getter = () => { subscriptions.add(effect); return value; }; const setter = (nextValue) => { value = nextValue; for (const sub of [...subscriptions]) { sub(); } }; return [getter, setter]; }
看來(lái)用上 Set 之后,我們的代碼精簡(jiǎn)了不少,so far so good。
這個(gè)優(yōu)化真的很考驗(yàn)讀者對(duì) js 這門(mén)復(fù)雜語(yǔ)言的掌握程度。首先,你得知道 forEach
和 for...of
雖然都是用來(lái)遍歷 Iterable 的數(shù)據(jù)結(jié)構(gòu),但是兩者之間還是有很多不同的。其中的一個(gè)很大的不同體現(xiàn)在「是否支持在遍歷中對(duì)源數(shù)據(jù)進(jìn)行動(dòng)態(tài)修改」。在這一點(diǎn)上,forEach
是不支持的,而for...of
是支持的。下面舉個(gè)簡(jiǎn)單的例子進(jìn)行說(shuō)明: 首先
const a = [1,2,3]; a.forEach(i=> { if(i === 3){ a.push(4)} console.log(i) }) // 1 // 2 // 3 console.log(a); // [1,2,3,4] for(const i of a){ if(i === 4){ a.push(5)} console.log(i) } // 1 // 2 // 3 // 4 // 5 console.log(a); // [1,2,3,4,5]
通過(guò)上面的對(duì)比,我們驗(yàn)證了上面提及的這兩者的不同點(diǎn):forEach
不會(huì)對(duì)源數(shù)據(jù)的動(dòng)態(tài)修改做出反應(yīng),而for...of
則是相反。
當(dāng)你知道 forEach
和 for...of
這一點(diǎn)區(qū)別后,結(jié)合我們實(shí)現(xiàn)響應(yīng)系統(tǒng)的這個(gè)上下文,顯然,我們這里更適合使用forEach
來(lái)遍歷 Set 這個(gè)數(shù)據(jù)結(jié)構(gòu)。于是,我們修改代碼,目前最終代碼如下:
let effect function createSignal(value) { const subscriptions = new Set(); const getter = () => { subscriptions.add(effect) return value; }; const setter = (nextValue) => { value = nextValue; [...subscriptions].forEach(sub=> sub()) }; return [getter, setter]; } function createEffect(fn){ effect = fn; fn() }
到目前為止,我們就可以交差了。因?yàn)?,如果用戶「不亂用」的話,這個(gè)迷你響應(yīng)系統(tǒng)是能夠運(yùn)行良好的。
何為「亂用」呢?好吧,讓我們現(xiàn)在來(lái)思考一下:「萬(wàn)一用戶嵌套式地創(chuàng)建 effect 呢?」
好,我們基于上面的最新代碼,用下面的代碼測(cè)試一下:
代碼片段2
const [count1, setCount1] = createSignal(0); const [count2, setCount2] = createSignal(0); createEffect(function count1Effect() { console.log(`count1: ${count1()}`) createEffect(function count2Effect(){ console.log(`count2: ${count2()}`) }) }) // count1: 0 // count2: 0 setCount1(1) // count1: 1 // count2: 0 // count2: 0 // 多了一次打印,為什么?
setCount1(1)
之后,我們期待應(yīng)該只打印兩次:
count1: 1
count2: 0
實(shí)際上卻是多了一次count2: 0
,這一次打印是哪里來(lái)的?問(wèn)題似乎出現(xiàn)在全局變量 effect
上 - 一旦 createEffect
嵌套調(diào)用了,那么,effect 的收集就發(fā)生了錯(cuò)亂。具體表現(xiàn)在,我們第一次調(diào)用 createEffect()
去創(chuàng)建 count1Effect 的時(shí)候,代碼執(zhí)行完畢后,此時(shí)全局變量 effect
指向 count2Effect。當(dāng)我們調(diào)用setCount1()
之后,我們就會(huì)通知 count1Effect,也就是調(diào)用count1Effect()
。這次調(diào)用過(guò)程中,我們就會(huì)再次去收集 count1 的訂閱者,此時(shí)訂閱者卻指向 count2Effect。好,這就是問(wèn)題之所在。
針對(duì)這個(gè)問(wèn)題,最簡(jiǎn)單的解決方法就是:調(diào)用完 effect 函數(shù)后,就釋放了全局變量的占用,如下:
function createEffect(fn){ effect = fn; fn(); effect = null; // 新增這一行 }
同時(shí),在收集 effect 函數(shù)地方加多一個(gè)防守:
function createSignal(value) { const subscriptions = new Set(); const getter = () => { !!effect && subscriptions.add(effect) // 新增防守 return value; }; const setter = (nextValue) => { value = nextValue; [...subscriptions].forEach(sub=> sub()) }; return [getter, setter]; }
如此一來(lái),就解決我們的問(wèn)題。解決這個(gè)問(wèn)題,還有另外一種解決方案 - 用「棧」的思想解決特定 js 值與所對(duì)應(yīng)的 effect 的匹配問(wèn)題。在這種方案中,我們將全局變量 effect
重命名為數(shù)組類型的 activeEffects
更符合語(yǔ)義:
let activeEffects = []; // 修改這一行 function createSignal(value) { const subscriptions = new Set(); const getter = () => { const currentEffect = activeEffects[activeEffects.length - 1]; // 新增這一行 subscriptions.add(currentEffect); return value; }; const setter = (nextValue) => { value = nextValue; [...subscriptions].forEach(sub=> sub()) }; return [getter, setter]; } function createEffect(fn){ activeEffects.push(fn); // 新增這一行 fn(); activeEffects.pop(); // 新增這一行 }
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),在代碼片段2中,如果我們接著去設(shè)置 count2
的值的話,count2Effect 會(huì)被執(zhí)行兩次。實(shí)際上,我覺(jué)得它僅僅被執(zhí)行一次是比較合理的。當(dāng)然,在這個(gè)示例代碼中,因?yàn)槲覀冎貜?fù)調(diào)用createEffect()
時(shí)候傳入是不同的,新的函數(shù)實(shí)例,因此被視為不同的 effect 也是理所當(dāng)然的。但是萬(wàn)一用戶在這種場(chǎng)景下(嵌套創(chuàng)建 effect)傳遞給我們的是同一個(gè) effect 函數(shù)實(shí)例的引用,我們能做到 『當(dāng)這個(gè) effect 函數(shù)所依賴的響應(yīng)值發(fā)生改變的時(shí)候,這個(gè) effect 函數(shù)只被調(diào)用一次嗎』?
答案是:“能”。而且我們目前已經(jīng)誤打誤撞地實(shí)現(xiàn)了這個(gè)功能。請(qǐng)看上面「用 Set 代替 數(shù)組」的優(yōu)化之后的結(jié)果:subscriptions.add(effect);
。這句代碼就通過(guò) Set 數(shù)據(jù)結(jié)構(gòu)自帶的去重特性,防止在嵌套創(chuàng)建 effect 場(chǎng)景下,如果用戶多次傳入的是同一個(gè) effect 函數(shù)實(shí)例引用,我們能夠保證它在響應(yīng)值的 subscriptions
中只會(huì)存在一個(gè)。因此,該 effect 函數(shù)只會(huì)被調(diào)用一次。
回到代碼片段2中,如果我們想 count2Effect 函數(shù)只會(huì)被執(zhí)行一次,那么我們?cè)撛趺醋瞿??答案是:?strong>傳遞一個(gè)外部的函數(shù)實(shí)例引用”。比如這樣:
const [count1, setCount1] = createSignal(0); const [count2, setCount2] = createSignal(0); function count2Effect(){ console.log(`count2: ${count2()}`) } createEffect(function count1Effect() { console.log(`count1: ${count1()}`) createEffect(count2Effect) })
好了,到了這里,我們基本上可以交差了,因?yàn)槲覀円呀?jīng)實(shí)現(xiàn)了響應(yīng)式系統(tǒng)的兩個(gè)基本要素:
實(shí)現(xiàn)值的包裹
訂閱值的變化
如果我們現(xiàn)在拿「代碼片段2」去測(cè)試,現(xiàn)在的結(jié)果應(yīng)該是符合我們的預(yù)期的。
從更高的標(biāo)準(zhǔn)來(lái)看,目前為止,前面實(shí)現(xiàn)的迷你型響應(yīng)系統(tǒng)還是比較粗糙的。其中的一個(gè)方面是:響應(yīng)的準(zhǔn)確性不高。下面我們著手來(lái)解決這個(gè)問(wèn)題。
如果讀者朋友能細(xì)心去把玩和測(cè)試我們目前實(shí)現(xiàn)的代碼,你會(huì)發(fā)現(xiàn),如果你對(duì)同一個(gè)響應(yīng)值多次設(shè)置同一個(gè)值的話,這個(gè)響應(yīng)值所對(duì)應(yīng)的 effect 都會(huì)被執(zhí)行:
代碼片段3
const [count1, setCount1] = createSignal(0); createEffect(function count1Effect(){ console.log(`count1: ${count1()}`) }) setCount1(1) // count1: 1 setCount1(1) // count1: 1
從上面的測(cè)試示例,我們可以看出,被觀察值沒(méi)有發(fā)生變化,我們還是執(zhí)行了 effect。這顯然是不夠準(zhǔn)確的。解決這個(gè)問(wèn)題也很簡(jiǎn)單,我們?cè)谠O(shè)置新值之前,加一個(gè)相等性判斷的防守 - 只有新值不等于舊值,我們才會(huì)設(shè)置新值。優(yōu)化如下:
function createSignal(value) { // ......省略很多代碼 const setter = (nextValue) => { if(nextValue !== value){ value = nextValue; [...subscriptions].forEach(sub=> sub()) } }; return [getter, setter]; }
或者,我們可以更進(jìn)一步,把判斷兩個(gè)值是否相等的決策權(quán)交給用戶。為了實(shí)現(xiàn)這個(gè)想法,我們可以讓用戶在創(chuàng)建響應(yīng)值的時(shí)候傳遞個(gè)用于判斷兩個(gè)值是否相等的函數(shù)進(jìn)來(lái)。如果用戶沒(méi)有傳遞,我們才使用 ===
作為相等性判斷的方法:
function createSignal(value, eqFn) { // ......省略很多代碼 const setter = (nextValue) => { let isChange if(typeof eqFn === 'function'){ isChange = !eqFn(value, nextValue); }else { isChange = nextValue !== value } if(isChange){ value = nextValue; [...subscriptions].forEach(sub=> sub()) } }; return [getter, setter]; }
經(jīng)過(guò)上面的優(yōu)化,我們?cè)倌?strong>代碼片段3去測(cè)試一下,結(jié)果是達(dá)到了我們的預(yù)期了: 第二次的 setCount1(1)
不會(huì)導(dǎo)致 effect 函數(shù)的執(zhí)行。
這里引入了「依賴管理」的概念?,F(xiàn)在,我們先不討論這個(gè)概念應(yīng)該如何理解,而是看看下面這個(gè)示例代碼:
代碼片段4
const [count1, setCount1] = createSignal(0); const [count2, setCount2] = createSignal(0); const [flag, setFlag] = createSignal(true); createEffect(function totalEffect(){ if(flag()){ console.log(`total : ${count1() + count2()}`); }else { console.log(`total : ${count1()}`); } }); setCount1(1); // total : 1 (第 1 次打印,符合預(yù)期) setCount2(1); // total : 2 (第 2 次打印,符合預(yù)期) setFlag(false); // total : 1 (第 3 次打印,符合預(yù)期) setCount1(2); // total : 2 (第 4 次打印,符合預(yù)期) setCount2(2); // total : 2 (第 5 次打印,不符合預(yù)期)
首先,我們得討論一下,什么是「依賴」?「依賴」其實(shí)是在描述 「effect 函數(shù)」跟「響應(yīng)值」之間的關(guān)系?,F(xiàn)在如果有這樣的觀點(diǎn):你「使用」了某個(gè)物品,我們就說(shuō)你「依賴」這個(gè)物品。那么,在上面的示例代碼中,totalEffect()
使用了響應(yīng)值count1
和 count2
,我們就可以說(shuō),totalEffect()
依賴(及物動(dòng)詞)了 count1
和 count2
。反過(guò)來(lái)我們也可以說(shuō),count1
和 count2
是totalEffect()
的依賴(名詞)。這就是「依賴管理」中「依賴」的含義 - 取名詞之義。
通過(guò)發(fā)散思維,我們不難發(fā)現(xiàn),effect 函數(shù)會(huì)依賴多個(gè)響應(yīng)值,一個(gè)響應(yīng)值會(huì)被多個(gè) effect 函數(shù)所依賴。effect 函數(shù) 與 響應(yīng)值之間的關(guān)系是「N:N」的關(guān)系。而這種關(guān)系是會(huì)隨著程序的執(zhí)行發(fā)生動(dòng)態(tài)變化的 - 之前依賴的響應(yīng)值,也許現(xiàn)在就不依賴了。又或者添加之間沒(méi)有的依賴項(xiàng)。就目前而言,我們還沒(méi)實(shí)現(xiàn)依賴管理的動(dòng)態(tài)化?;氐奖臼纠校?code>setFlag(false);調(diào)用之前,我們的 totalEffect 是依賴兩個(gè)響應(yīng)值 count1
和 count2
。而在此之后,實(shí)際上它只依賴 count1
。但是,從第 5 次的打印來(lái)看,setCount2(2)
還是通知到了 totalEffect()
。實(shí)際上,因?yàn)槲?totalEffect()
并沒(méi)有使用 count2
了,所以,我并不需要對(duì) count2
值的改變做出響應(yīng)。
那我們?cè)撊绾螌?shí)現(xiàn) effect 函數(shù)跟響應(yīng)值依賴關(guān)系的動(dòng)態(tài)化管理呢?基本思路就是:我們需要在 effect 函數(shù)執(zhí)行之前,先清空之前的依賴關(guān)系。然后,在本次執(zhí)行完畢,構(gòu)建一個(gè)新的依賴關(guān)系圖。
就目前而言,某個(gè)響應(yīng)值被哪些 effect 函數(shù)所依賴,這個(gè)關(guān)系是在創(chuàng)建響應(yīng)值時(shí)候所閉包住的 subscriptions
數(shù)組中體現(xiàn)的。而一個(gè) effect 函數(shù)所依賴了哪些響應(yīng)值,這個(gè)依賴關(guān)系并沒(méi)有數(shù)據(jù)結(jié)構(gòu)來(lái)體現(xiàn)。所以,我們得先實(shí)現(xiàn)這個(gè)。我們要在創(chuàng)建 effect 的時(shí)候,為每一個(gè) effect 函數(shù)創(chuàng)建一個(gè)與一一對(duì)應(yīng)的依賴管理器,命名為 effectDependencyManager
:
function createEffect(fn, eqFn) { const effectDependencyManager = { dependencies: new Set(), run() { activeEffect = effectDependencyManager; fn(); // 執(zhí)行的時(shí)候再重建新的依賴關(guān)系圖 activeEffect = null; } }; effectDependencyManager.run(); }
然后在 effect 函數(shù)被收集到 subscriptions
數(shù)組的時(shí)候,也要把subscriptions
數(shù)組放到 effectDependencyManager.dependencies
數(shù)組里面,以便于當(dāng) effect 函數(shù)不依賴某個(gè)響應(yīng)值的時(shí)候,也能從該響應(yīng)值的subscriptions
數(shù)組反向找到自己,然后刪除自己。
function createSignal(value, eqFn) { const subscriptions = new Set(); const getter = () => { if (activeEffect) { activeEffect.dependencies.add(subscriptions); subscriptions.add(activeEffect); } return value; }; // ......省略其他代碼 }
上面已經(jīng)提到了,為了動(dòng)態(tài)更新一個(gè) effect 函數(shù)跟其他響應(yīng)值的依賴關(guān)系,我們需要在它的每個(gè)次執(zhí)行前「先清除所有的依賴關(guān)系,然后再重新構(gòu)建新的依賴圖」?,F(xiàn)在,就差「清除 effect 函數(shù)所有的依賴關(guān)系」這一步了。為了實(shí)現(xiàn)這一步,我們要實(shí)現(xiàn)一個(gè) cleanup()
函數(shù):
function cleanup(effectDependencyManager) { const deps = effectDependencyManager.dependencies; deps.forEach(sub=> sub.delete(effectDependencyManager)) effectDependencyManager.dependencies = new Set(); }
上面的代碼意圖已經(jīng)很明確了。cleanup()
函數(shù)要實(shí)現(xiàn)的就是遍歷 effect 函數(shù)上一輪所依賴的響應(yīng)值,然后從響應(yīng)值的subscriptions
數(shù)組中把自己刪除掉。最后,清空effectDependencyManager.dependencies
數(shù)組。
最后,我們?cè)?effect 函數(shù)調(diào)用之前,調(diào)用一下這個(gè) cleanup()
:
function createEffect(fn, eqFn) { const effectDependencyManager = { dependencies: [], run() { cleanup(effectDependencyManager); activeEffect = effectDependencyManager; fn(); // 執(zhí)行的時(shí)候再重建新的依賴關(guān)系圖 activeEffect = null; } }; effectDependencyManager.run(); }
我們?cè)倌?strong>代碼片段4來(lái)測(cè)試一下,現(xiàn)在的打印結(jié)果應(yīng)該是符合我們得預(yù)期了 - 當(dāng)我們調(diào)用setFlag(false);
之后,我們實(shí)現(xiàn)了 totalEffect 的依賴關(guān)系圖的動(dòng)態(tài)更新。在新的依賴關(guān)系圖中,我們已經(jīng)不依賴響應(yīng)值count2
了。所以,當(dāng)count2
的值發(fā)生改變后,totalEffect 函數(shù)也不會(huì)被重新執(zhí)行。
當(dāng)前,我們引入了新的數(shù)據(jù)結(jié)構(gòu) effectDependencyManager
。這會(huì)導(dǎo)致我們之前所已經(jīng)實(shí)現(xiàn)的某個(gè)功能被回退掉了。哪個(gè)呢?答案是:“同一個(gè) effect 函數(shù)實(shí)例不被重復(fù)入隊(duì)”。
為什么?因?yàn)椋F(xiàn)在我們添加到 subscriptions
集合的元素不再是用戶傳遞進(jìn)來(lái)的 effect 函數(shù),而是經(jīng)過(guò)我們包裝后的依賴管理器 effectDependencyManager
。而這個(gè)依賴管理器每次在用戶在調(diào)用 createEffect()
的時(shí)候都生成一個(gè)新的實(shí)例。這就導(dǎo)致了之前利用 Set 集合的天生去重能力就喪失掉了。所以,接下來(lái),我們需要把這塊的功能給補(bǔ)回來(lái)。首先,我們?cè)?effectDependencyManager
身上新加一個(gè)屬性,用它來(lái)保存用戶傳進(jìn)來(lái)的函數(shù)實(shí)例引用:
function createEffect(fn) { const effectDependencyManager = { dependencies: new Set(), run() { // 在執(zhí)行 effect 之前,清除上一次的依賴關(guān)系 cleanup(effectDependencyManager); activeEffect = effectDependencyManager; // activeEffects.push(effectDependencyManager); fn(); // 執(zhí)行的時(shí)候再重建新的依賴關(guān)系圖 activeEffect = null; }, origin: fn // 新增一行 }; effectDependencyManager.run(); }
其次,我們?cè)诎?effectDependencyManager
添加到響應(yīng)值的 subscriptions
集合去之前,我們先做個(gè)手動(dòng)的去重防守:
function createSignal(value, eqFn) { const subscriptions = new Set(); const getter = ()=>{ if (activeEffect) { const originEffects = [] for (const effectManager of subscriptions) { originEffects.push(effectManager.origin) } const hadSubscribed = originEffects.includes(activeEffect.origin) if (!hadSubscribed) { activeEffect.dependencies.add(subscriptions); subscriptions.add(activeEffect); } } return value; } // ...省略其他代碼 return [getter, setter]; }
至此,我們把丟失的「同一個(gè) effect 函數(shù)實(shí)例不被重復(fù)入隊(duì)」功能補(bǔ)回來(lái)了。
換句話說(shuō),我們需要支持用戶向響應(yīng)值的 setter 傳入函數(shù)來(lái)訪問(wèn)舊值,然后計(jì)算出要設(shè)置的值。用代碼來(lái)說(shuō),即支持下面的 API 語(yǔ)法:
const [count1, setCount1] = createSignal(0); setCount1(c=> c + 1);
實(shí)現(xiàn)這個(gè)特性很簡(jiǎn)單,我們判斷用戶傳進(jìn)來(lái)的 nextValue
值的類型,區(qū)別處理即可:
function createSignal(value, eqFn) { // ......省略其他代碼 const setter = (nextValue)=>{ nextValue = typeof nextValue === 'function' ? nextValue(value) : nextValue;// 新增一行 let isChange; if (typeof eqFn === 'function') { isChange = !eqFn(value, nextValue); } else { isChange = nextValue !== value } if (isChange) { value = nextValue; [...subscriptions].forEach(sub=>sub.run()) } }; return [getter, setter]; }
計(jì)算屬性(computed)也有很多叫法,它還可以稱之為:
Derivations
Memos
pure computed
在這里我們沿用 solidjs 的叫法: memo
。 這是一個(gè)很常見(jiàn)和廣為接受的概念了。在這,我們一并實(shí)現(xiàn)它。其實(shí),在我們當(dāng)前這個(gè)框架上實(shí)現(xiàn)這個(gè)特性是比較簡(jiǎn)單的 - 本質(zhì)上是對(duì) createEffect
函數(shù)的二次封裝:
function createMemo(fn){ const [result, setResult] = createSingal(); createEffect(()=> { setResult(fn()) }); return result; }
你可以用下面的代碼去測(cè)試一下:
const [count1, setCount1] = createSignal(0); const [count2, setCount2] = createSignal(0); const total = createMemo(() => count1() + count2()); createEffect(()=> { console.log(`total: ${total()}`) }); // total: 0 setCount1(1); // total: 1 setCount2(100); // total: 101
感謝各位的閱讀,以上就是“怎么用代碼實(shí)現(xiàn)一個(gè)迷你響應(yīng)式系統(tǒng)vue”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)怎么用代碼實(shí)現(xiàn)一個(gè)迷你響應(yīng)式系統(tǒng)vue這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。