溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么用代碼實(shí)現(xiàn)一個(gè)迷你響應(yīng)式系統(tǒng)vue

發(fā)布時(shí)間:2023-03-28 16:17:18 來(lái)源:億速云 閱讀:109 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要講解了“怎么用代碼實(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ǔ)。

代碼實(shí)現(xiàn)

實(shí)現(xiàn)值的包裹

包裹 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。

小優(yōu)化

細(xì)心的讀者可能會(huì)注意到,其實(shí)上面的代碼還是可以有優(yōu)化的空間的 - 我們可以讓它更精簡(jiǎn)和健壯。

用 Set 代替數(shù)組

首先我們看看這段防守代碼:

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。

用 forEach 代替 for...of

這個(gè)優(yōu)化真的很考驗(yàn)讀者對(duì) js 這門(mén)復(fù)雜語(yǔ)言的掌握程度。首先,你得知道 forEachfor...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)你知道 forEachfor...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 呢?」

支持 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(); // 新增這一行
}
同一個(gè) effect 函數(shù)實(shí)例不被重復(fù)入隊(duì)

細(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) 
})

小結(jié)

好了,到了這里,我們基本上可以交差了,因?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ù)期的。

提高響應(yīng)的準(zhǔn)確性

從更高的標(biāo)準(zhǔn)來(lái)看,目前為止,前面實(shí)現(xiàn)的迷你型響應(yīng)系統(tǒng)還是比較粗糙的。其中的一個(gè)方面是:響應(yīng)的準(zhǔn)確性不高。下面我們著手來(lái)解決這個(gè)問(wèn)題。

避免不必要的 rerun

如果讀者朋友能細(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í)行。

動(dòng)態(tài)的依賴管理

這里引入了「依賴管理」的概念?,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)值count1count2,我們就可以說(shuō),totalEffect()依賴(及物動(dòng)詞)了 count1count2。反過(guò)來(lái)我們也可以說(shuō),count1count2totalEffect()的依賴(名詞)。這就是「依賴管理」中「依賴」的含義 - 取名詞之義。

通過(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)值 count1count2。而在此之后,實(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í)行。

修復(fù)功能回退

當(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)了。

附加特性

支持基于舊值來(lái)產(chǎn)生新值

換句話說(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ì)算屬性

計(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)注!

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

vue
AI