亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

教你60行代碼實現(xiàn)一個迷你響應式系統(tǒng)vue

 更新時間:2023年03月28日 13:56:34   作者:鯊叔  
這篇文章主要為大家介紹了教你60行代碼實現(xiàn)一個迷你響應式系統(tǒng)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪<BR>

前言

沒有耐心看完文章,想直接把玩?鯊叔傾情奉獻給你:60 行代碼實現(xiàn)迷你響應式系統(tǒng)。

前些天,去面試字節(jié)的前端崗位。面試官看到我在簡歷上提及了深入理解響應式系統(tǒng)。于是乎,他問我能不能簡單地寫一個迷你的響應式類庫。這正中我的下懷,于是我信手拈來,巴拉巴拉地寫了出來。為什么我能這么快寫出來呢?那是因為我前段時間剛好研究過響應式系統(tǒng)的核心原理。這里面涉獵的庫有 mobx,solid,vue 等等。

時下,solidjs 和 quickjs 的作者將 signal 和 fine-grained-reactivity 等概念炒得火熱。但是,無論怎么炒,其實響應式系統(tǒng)的內(nèi)核原理是萬變不離其宗的。變是變在,各個響應式 UI 庫實現(xiàn)響應式系統(tǒng)所采用的技術方案,附加特性方面表現(xiàn)得不同。以不變應萬變,我們只要掌握好響應式系統(tǒng)的內(nèi)核原理即可。下面,讓我娓娓道來,用大概 60 行代碼為你循序漸進地實現(xiàn)一個精簡版的響應式系統(tǒng)。

基本定義

什么是響應式系統(tǒng)?學術上的定義,我們就不細究了。通過縱觀前端業(yè)界對響應系統(tǒng)的實現(xiàn),其實,這個定義是很簡單的。 無非是 - 一個系統(tǒng),它能夠?qū)尤脒@個系統(tǒng)的 js 值的變化自動地做出反應的話,那么這個系統(tǒng)就可以稱之為「響應式系統(tǒng)」。

基本要素

從上面的基本定義來看,響應式系統(tǒng)就包含兩個基本的,必不可少的要素:

  • 被觀察的值
  • 能夠響應值發(fā)生變化的能力

「能被觀察的值」在不同的 UI 庫中叫法不一樣。比如:

  • mobx 中稱之為「observables」
  • solidjs 稱之為「signal」
  • vue 稱之為「ref」
  • recoil 稱之為 「atom」
  • 還有稱之為「subjects」或者「state」

不管你怎么叫,它終究還是一個能被觀察的 「js 值」。顯然, 原始的 js 值是沒有響應性的,這里的「能被觀察」正是需要我們自己去封裝實現(xiàn)的。這里的實現(xiàn)的基本思路就是「包裹」。展開說,就是你想某個 js 值能被觀察,那么它就必須被「某個東西」包裹住,然后與之配合,用戶消費的是包裹后的產(chǎn)物而不是原始值。

實現(xiàn)「包裹」的方式不一樣,那么最終提供給用戶的 API 的風格就不一樣。不同風格的 API 所帶來的 DX 不同。比如,vue3 里面,它的響應式系統(tǒng)是基于瀏覽器的原生 API Proxy 來實現(xiàn)值的包裹的。在這中技術方案下,用戶使用原生的 js 值訪問語法和賦值語法即可:

const proxyVal = new Proxy(originVal, {
    get(){},
    set(){}
});
// 讀值
console.log(proxyVal);
// 寫值
proxyVal = newVal;

跟 vue 不同,solidjs 自己實現(xiàn)了一套顯式的讀和寫 API:

const [val, setVal] = createSignal(originVal);
// 讀值
console.log(val());
// 寫值
setVal(newVal)

以上是第一基本要素。第二個基本要素是,我們得有響應被觀察值發(fā)生變化的能力。這種能力主要體現(xiàn)在當我們所消費的 js 值發(fā)生了變化后,我們要根據(jù)特定的上下文來做出對應的反應。js 值被消費的最常見的地方就是 js 語句。如果我們能讓這個語句重新再執(zhí)行一次,那么它就能拿到最新的值。這就是所謂的響應式。那如果能夠讓一個 js 語句再執(zhí)行一遍呢?答案是:“把它放在函數(shù)里面,重新調(diào)用這個函數(shù)即可”。

上面所提到的「函數(shù)」就是函數(shù)式編程概念里面的「副作用」(effect)。還是老樣子,同一個東西,不同的類庫有不同的叫法。effect 又可以稱之為:

  • reaction
  • consumer(值的消費者)
  • listener(值的監(jiān)聽者)

等等。一般而言,副作用是要被響應式系統(tǒng)接管起來的,等到被觀察的 js 值發(fā)生變化的時候,我們再去調(diào)用它。從而實現(xiàn)了所謂的響應能力。這個用于接管的 API,不同的類庫有不同的叫法:

  • createEffect
  • consume
  • addListener
  • subscribe

以上是對響應式系統(tǒng)的最基本的兩個要素的闡述。下面,我們就從這個認知基礎出發(fā),循序漸進地用 60 行代碼去實現(xiàn)一個迷你響應系統(tǒng)。為了提高逼格,我們沿用 solidjs 響應式系統(tǒng)所采用的相關術語。

代碼實現(xiàn)

實現(xiàn)值的包裹

包裹 js 值的根本目的就是為了監(jiān)聽用戶對這些值的「讀」和「寫」的兩個動作:

function createSignal(value) {
  const getter = () => {
    console.log('我監(jiān)聽到讀值了')
    return value;
  };
  const setter = (nextValue) => {
   console.log('我監(jiān)聽到寫值了')
   value = nextValue;
  };
  return [getter, setter]; 
}
const [count, setCount] = createSignal(0)
//讀
count()
// 我監(jiān)聽到讀值了
//寫
setCount(1)
// 我監(jiān)聽到寫值了

可以說,我們的這種 API 設計改變了用戶對 js 值的讀寫習慣,甚至可以說有點強迫性。很多人都不習慣讀值的這種語法是一個函數(shù)調(diào)用。沒辦法,拿人手短,吃人嘴軟,習慣就好(不就是多敲連兩個字符嗎?哈哈)。

通過這種帶有一點強制意味的 API 設計,我們能夠監(jiān)聽到用戶對所觀察值的讀和寫。

其實,上面的短短的幾行代碼是本次要實現(xiàn)的迷你型響應系統(tǒng)的奠基框架。因為,剩下要做的,我們就是不斷往 setter 和 getter 的函數(shù)體里面堆砌代碼,以實現(xiàn)響應式系統(tǒng)的基本功能。

訂閱值的變化

用戶對 js 值的消費一般是發(fā)生在語句中。為了重新執(zhí)行這些語句,我們需要提供一個 API 給用戶來將語句封裝起來成為一個函數(shù),然后把這個函數(shù)當做值存儲起來,在未來的某個時刻由系統(tǒng)去調(diào)用這個函數(shù)。當然,順應「語句」的語義,我們應該在將語句封裝在函數(shù)里面之后,應該馬上執(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()
}

至此,我們算是實現(xiàn)了響應系統(tǒng)的基本框架:

  • 一個可以幫助 js 值被觀察的 API
  • 一個輔助用戶創(chuàng)建 effect 的 API

熟悉設計模式的讀者可以看出,這個框架的背后其實就是「訂閱-發(fā)布模式」 - 系統(tǒng)在用戶「讀值」的時候去做訂閱,在用戶「寫值」的時候去通知所有的訂閱者(effect)。

上面的代碼看起來好像沒問題。不信?我們測試一下:

代碼片段1

const [count, setCount] = createSignal(0)
createEffect(()=> {
    console.log(`count: ${count()}`);
})
// 打印一次:count: 0
setCount(1)
// ?

在打問號的地方,我們期待它是打印一次count: 1。但是實際上它一直在打印,導致頁面卡死了??磥恚?code>setCount(1)導致了無限循環(huán)調(diào)用了。仔細分析一下,我們會發(fā)現(xiàn),導致無限循環(huán)調(diào)用的原因在于:setCount(1) 會導致系統(tǒng)遍歷subscriptions數(shù)組,去調(diào)用每一個 effect。而調(diào)用 effect() 又會產(chǎn)生一次讀值。一旦讀值,我們就會把當前全局變量effect push 到subscriptions數(shù)組。這就會導致了我們的 subscriptions數(shù)組永遠遍歷不完。我們可以通過組合下面兩個防守來解決這個問題:

  • 防止同一個 effect 被重復 push 到 subscriptions 數(shù)組里面了。
  • 先對 subscriptions 數(shù)組做淺拷貝,再遍歷這個淺拷貝的數(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]; 
}

我們再用上面「代碼片段1」去測試一下,你會發(fā)現(xiàn),結果是符合預期的,沒有 bug。

小優(yōu)化

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

用 Set 代替數(shù)組

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

if(!subscriptions.includes(effect)){
    subscriptions.push(effect)
}  

這段代碼的目的不言而喻,我們不希望 subscriptions 存在「重復的」effect。一提到去重相關的需求,我們得馬上想到「自帶去重功能的」,ES6 規(guī)范添加的新的數(shù)據(jù)結構 「Set」。于是,我們用 Set 來代替數(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]; 
}

看來用上 Set 之后,我們的代碼精簡了不少,so far so good。

用 forEach 代替 for...of

這個優(yōu)化真的很考驗讀者對 js 這門復雜語言的掌握程度。首先,你得知道 forEachfor...of 雖然都是用來遍歷 Iterable 的數(shù)據(jù)結構,但是兩者之間還是有很多不同的。其中的一個很大的不同體現(xiàn)在「是否支持在遍歷中對源數(shù)據(jù)進行動態(tài)修改」。在這一點上,forEach 是不支持的,而for...of 是支持的。下面舉個簡單的例子進行說明: 首先

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]

通過上面的對比,我們驗證了上面提及的這兩者的不同點:forEach 不會對源數(shù)據(jù)的動態(tài)修改做出反應,而for...of 則是相反。

當你知道 forEachfor...of 這一點區(qū)別后,結合我們實現(xiàn)響應系統(tǒng)的這個上下文,顯然,我們這里更適合使用forEach 來遍歷 Set 這個數(shù)據(jù)結構。于是,我們修改代碼,目前最終代碼如下:

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

到目前為止,我們就可以交差了。因為,如果用戶「不亂用」的話,這個迷你響應系統(tǒng)是能夠運行良好的。

何為「亂用」呢?好吧,讓我們現(xiàn)在來思考一下:「萬一用戶嵌套式地創(chuàng)建 effect 呢?」

支持 effect 嵌套

好,我們基于上面的最新代碼,用下面的代碼測試一下:

代碼片段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) 之后,我們期待應該只打印兩次:

count1: 1
count2: 0

實際上卻是多了一次count2: 0,這一次打印是哪里來的?問題似乎出現(xiàn)在全局變量 effect 上 - 一旦 createEffect 嵌套調(diào)用了,那么,effect 的收集就發(fā)生了錯亂。具體表現(xiàn)在,我們第一次調(diào)用 createEffect() 去創(chuàng)建 count1Effect 的時候,代碼執(zhí)行完畢后,此時全局變量 effect指向 count2Effect。當我們調(diào)用setCount1()之后,我們就會通知 count1Effect,也就是調(diào)用count1Effect()。這次調(diào)用過程中,我們就會再次去收集 count1 的訂閱者,此時訂閱者卻指向 count2Effect。好,這就是問題之所在。

針對這個問題,最簡單的解決方法就是:調(diào)用完 effect 函數(shù)后,就釋放了全局變量的占用,如下:

function createEffect(fn){
    effect = fn;
    fn();
    effect = null; // 新增這一行
}

同時,在收集 effect 函數(shù)地方加多一個防守:

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]; 
}

如此一來,就解決我們的問題。解決這個問題,還有另外一種解決方案 - 用「?!沟乃枷虢鉀Q特定 js 值與所對應的 effect 的匹配問題。在這種方案中,我們將全局變量 effect 重命名為數(shù)組類型的 activeEffects更符合語義:

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(); // 新增這一行
}

同一個 effect 函數(shù)實例不被重復入隊

細心的讀者可能會發(fā)現(xiàn),在代碼片段2中,如果我們接著去設置 count2 的值的話,count2Effect 會被執(zhí)行兩次。實際上,我覺得它僅僅被執(zhí)行一次是比較合理的。當然,在這個示例代碼中,因為我們重復調(diào)用createEffect()時候傳入是不同的,新的函數(shù)實例,因此被視為不同的 effect 也是理所當然的。但是萬一用戶在這種場景下(嵌套創(chuàng)建 effect)傳遞給我們的是同一個 effect 函數(shù)實例的引用,我們能做到 『當這個 effect 函數(shù)所依賴的響應值發(fā)生改變的時候,這個 effect 函數(shù)只被調(diào)用一次嗎』?

答案是:“能”。而且我們目前已經(jīng)誤打誤撞地實現(xiàn)了這個功能。請看上面「用 Set 代替 數(shù)組」的優(yōu)化之后的結果:subscriptions.add(effect);。這句代碼就通過 Set 數(shù)據(jù)結構自帶的去重特性,防止在嵌套創(chuàng)建 effect 場景下,如果用戶多次傳入的是同一個 effect 函數(shù)實例引用,我們能夠保證它在響應值的 subscriptions 中只會存在一個。因此,該 effect 函數(shù)只會被調(diào)用一次。

回到代碼片段2中,如果我們想 count2Effect 函數(shù)只會被執(zhí)行一次,那么我們該怎么做呢?答案是:“傳遞一個外部的函數(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) 
})

小結

好了,到了這里,我們基本上可以交差了,因為我們已經(jīng)實現(xiàn)了響應式系統(tǒng)的兩個基本要素:

  • 實現(xiàn)值的包裹
  • 訂閱值的變化

如果我們現(xiàn)在拿「代碼片段2」去測試,現(xiàn)在的結果應該是符合我們的預期的。

提高響應的準確性

從更高的標準來看,目前為止,前面實現(xiàn)的迷你型響應系統(tǒng)還是比較粗糙的。其中的一個方面是:響應的準確性不高。下面我們著手來解決這個問題。

避免不必要的 rerun

如果讀者朋友能細心去把玩和測試我們目前實現(xiàn)的代碼,你會發(fā)現(xiàn),如果你對同一個響應值多次設置同一個值的話,這個響應值所對應的 effect 都會被執(zhí)行:

代碼片段3

const [count1, setCount1] = createSignal(0);
createEffect(function count1Effect(){
    console.log(`count1: ${count1()}`)
}) 
setCount1(1)
// count1: 1
setCount1(1)
// count1: 1

從上面的測試示例,我們可以看出,被觀察值沒有發(fā)生變化,我們還是執(zhí)行了 effect。這顯然是不夠準確的。解決這個問題也很簡單,我們在設置新值之前,加一個相等性判斷的防守 - 只有新值不等于舊值,我們才會設置新值。優(yōu)化如下:

function createSignal(value) {
  // ......省略很多代碼
  const setter = (nextValue) => {
     if(nextValue !== value){
         value = nextValue;
         [...subscriptions].forEach(sub=> sub())
     }
  };
  return [getter, setter]; 
}

或者,我們可以更進一步,把判斷兩個值是否相等的決策權交給用戶。為了實現(xiàn)這個想法,我們可以讓用戶在創(chuàng)建響應值的時候傳遞個用于判斷兩個值是否相等的函數(shù)進來。如果用戶沒有傳遞,我們才使用 === 作為相等性判斷的方法:

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)過上面的優(yōu)化,我們再拿代碼片段3去測試一下,結果是達到了我們的預期了: 第二次的 setCount1(1) 不會導致 effect 函數(shù)的執(zhí)行。

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

這里引入了「依賴管理」的概念?,F(xiàn)在,我們先不討論這個概念應該如何理解,而是看看下面這個示例代碼:

代碼片段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 次打印,符合預期)
setCount2(1);
// total : 2 (第 2 次打印,符合預期)
setFlag(false);
// total : 1 (第 3 次打印,符合預期)
setCount1(2);
// total : 2 (第 4 次打印,符合預期)
setCount2(2);
// total : 2 (第 5 次打印,不符合預期)

首先,我們得討論一下,什么是「依賴」?「依賴」其實是在描述 「effect 函數(shù)」跟「響應值」之間的關系。現(xiàn)在如果有這樣的觀點:你「使用」了某個物品,我們就說你「依賴」這個物品。那么,在上面的示例代碼中,totalEffect() 使用了響應值count1count2,我們就可以說,totalEffect()依賴(及物動詞)了 count1count2。反過來我們也可以說,count1count2totalEffect()的依賴(名詞)。這就是「依賴管理」中「依賴」的含義 - 取名詞之義。

通過發(fā)散思維,我們不難發(fā)現(xiàn),effect 函數(shù)會依賴多個響應值,一個響應值會被多個 effect 函數(shù)所依賴。effect 函數(shù) 與 響應值之間的關系是「N:N」的關系。而這種關系是會隨著程序的執(zhí)行發(fā)生動態(tài)變化的 - 之前依賴的響應值,也許現(xiàn)在就不依賴了。又或者添加之間沒有的依賴項。就目前而言,我們還沒實現(xiàn)依賴管理的動態(tài)化?;氐奖臼纠校?code>setFlag(false);調(diào)用之前,我們的 totalEffect 是依賴兩個響應值 count1count2。而在此之后,實際上它只依賴 count1。但是,從第 5 次的打印來看,setCount2(2) 還是通知到了 totalEffect()。實際上,因為我 totalEffect()并沒有使用 count2 了,所以,我并不需要對 count2 值的改變做出響應。

那我們該如何實現(xiàn) effect 函數(shù)跟響應值依賴關系的動態(tài)化管理呢?基本思路就是:我們需要在 effect 函數(shù)執(zhí)行之前,先清空之前的依賴關系。然后,在本次執(zhí)行完畢,構建一個新的依賴關系圖。

就目前而言,某個響應值被哪些 effect 函數(shù)所依賴,這個關系是在創(chuàng)建響應值時候所閉包住的 subscriptions 數(shù)組中體現(xiàn)的。而一個 effect 函數(shù)所依賴了哪些響應值,這個依賴關系并沒有數(shù)據(jù)結構來體現(xiàn)。所以,我們得先實現(xiàn)這個。我們要在創(chuàng)建 effect 的時候,為每一個 effect 函數(shù)創(chuàng)建一個與一一對應的依賴管理器,命名為 effectDependencyManager:

function createEffect(fn, eqFn) {
  const effectDependencyManager = {
    dependencies: new Set(),
    run() {
      activeEffect = effectDependencyManager;
      fn(); // 執(zhí)行的時候再重建新的依賴關系圖
      activeEffect = null;
    }
  };
  effectDependencyManager.run();
}

然后在 effect 函數(shù)被收集到 subscriptions 數(shù)組的時候,也要把subscriptions 數(shù)組放到 effectDependencyManager.dependencies 數(shù)組里面,以便于當 effect 函數(shù)不依賴某個響應值的時候,也能從該響應值的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)提到了,為了動態(tài)更新一個 effect 函數(shù)跟其他響應值的依賴關系,我們需要在它的每個次執(zhí)行前「先清除所有的依賴關系,然后再重新構建新的依賴圖」?,F(xiàn)在,就差「清除 effect 函數(shù)所有的依賴關系」這一步了。為了實現(xiàn)這一步,我們要實現(xiàn)一個 cleanup()函數(shù):

function cleanup(effectDependencyManager) {
  const deps = effectDependencyManager.dependencies;
  deps.forEach(sub=&gt; sub.delete(effectDependencyManager))
  effectDependencyManager.dependencies = new Set();
 }

上面的代碼意圖已經(jīng)很明確了。cleanup()函數(shù)要實現(xiàn)的就是遍歷 effect 函數(shù)上一輪所依賴的響應值,然后從響應值的subscriptions數(shù)組中把自己刪除掉。最后,清空effectDependencyManager.dependencies 數(shù)組。

最后,我們在 effect 函數(shù)調(diào)用之前,調(diào)用一下這個 cleanup()

function createEffect(fn, eqFn) {
  const effectDependencyManager = {
    dependencies: [],
    run() {
      cleanup(effectDependencyManager);
      activeEffect = effectDependencyManager;
      fn(); // 執(zhí)行的時候再重建新的依賴關系圖
      activeEffect = null;
    }
  };
  effectDependencyManager.run();
}

我們再拿代碼片段4來測試一下,現(xiàn)在的打印結果應該是符合我們得預期了 - 當我們調(diào)用setFlag(false); 之后,我們實現(xiàn)了 totalEffect 的依賴關系圖的動態(tài)更新。在新的依賴關系圖中,我們已經(jīng)不依賴響應值count2了。所以,當count2的值發(fā)生改變后,totalEffect 函數(shù)也不會被重新執(zhí)行。

修復功能回退

當前,我們引入了新的數(shù)據(jù)結構 effectDependencyManager。這會導致我們之前所已經(jīng)實現(xiàn)的某個功能被回退掉了。哪個呢?答案是:“同一個 effect 函數(shù)實例不被重復入隊”。

為什么?因為,現(xiàn)在我們添加到 subscriptions 集合的元素不再是用戶傳遞進來的 effect 函數(shù),而是經(jīng)過我們包裝后的依賴管理器 effectDependencyManager。而這個依賴管理器每次在用戶在調(diào)用 createEffect() 的時候都生成一個新的實例。這就導致了之前利用 Set 集合的天生去重能力就喪失掉了。所以,接下來,我們需要把這塊的功能給補回來。首先,我們在 effectDependencyManager 身上新加一個屬性,用它來保存用戶傳進來的函數(shù)實例引用:

function createEffect(fn) {
    const effectDependencyManager = {
        dependencies: new Set(),
        run() {
            // 在執(zhí)行 effect 之前,清除上一次的依賴關系
            cleanup(effectDependencyManager);
            activeEffect = effectDependencyManager;
            // activeEffects.push(effectDependencyManager);
            fn();
            // 執(zhí)行的時候再重建新的依賴關系圖
            activeEffect = null;
        },
        origin: fn // 新增一行
    };
    effectDependencyManager.run();
}

其次,我們在把 effectDependencyManager 添加到響應值的 subscriptions 集合去之前,我們先做個手動的去重防守:

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];
}

至此,我們把丟失的「同一個 effect 函數(shù)實例不被重復入隊」功能補回來了。

附加特性

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

換句話說,我們需要支持用戶向響應值的 setter 傳入函數(shù)來訪問舊值,然后計算出要設置的值。用代碼來說,即支持下面的 API 語法:

const [count1, setCount1] = createSignal(0);
setCount1(c=> c + 1);

實現(xiàn)這個特性很簡單,我們判斷用戶傳進來的 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];
}

派生值/計算屬性

計算屬性(computed)也有很多叫法,它還可以稱之為:

  • Derivations
  • Memos
  • pure computed

在這里我們沿用 solidjs 的叫法: memo。 這是一個很常見和廣為接受的概念了。在這,我們一并實現(xiàn)它。其實,在我們當前這個框架上實現(xiàn)這個特性是比較簡單的 - 本質(zhì)上是對 createEffect 函數(shù)的二次封裝:

function createMemo(fn){
    const [result, setResult] = createSingal();
    createEffect(()=> {
        setResult(fn())
    });
    return result;
}

你可以用下面的代碼去測試一下:

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

總結

以上就是用 60 行代碼循序漸進去實現(xiàn)一個迷你響應式系統(tǒng)的全部過程。在這個這個迷你響應式系統(tǒng)里面,我們實現(xiàn)的功能有:

  • 對原始 js 值進行響應式包裹,被包裹的 js 值我們可以稱之為「響應值」;
  • 響應值發(fā)生真正改變的時候,我們的 effect 能被重新執(zhí)行;
  • 保證多個響應值與它們自己的 effect 之間的對應關系是正確的,互不影響;
  • 支持嵌套式地創(chuàng)建 effect。且實現(xiàn)了該場景下,同一個 effect 函數(shù)實例不被重復入隊;
  • 實現(xiàn) effect 函數(shù)的依賴關系的動態(tài)更新;
  • 支持計算屬性特性。

別小看這 60 行代碼,它就是響應式系統(tǒng)的極度精簡的內(nèi)核,幾乎可以講是字字珠璣。在這個內(nèi)核模型下,我們可以很容易地擴展更多附加特性,比如像 vue3 的響應式系統(tǒng)里面的 watch, scheduler 等特性。

最后,總結一下該代碼實現(xiàn)的原理。其實還是大名鼎鼎的「閉包」。通過閉包,這個迷你響應式系統(tǒng)在內(nèi)存中保存著一條用戶看不見的結構數(shù)據(jù)關系鏈,如下:

基于上面的結構數(shù)據(jù)關系鏈圖,我們完全可以實現(xiàn)一版 OOP 的迷你響應式系統(tǒng),這個任務就交給讀者朋友來挑戰(zhàn)了。

以上就是教你60行代碼實現(xiàn)一個迷你響應式系統(tǒng)vue的詳細內(nèi)容,更多關于vue迷你響應式系統(tǒng)的資料請關注腳本之家其它相關文章!

相關文章

  • VueX?mapGetters獲取Modules中的Getters方式

    VueX?mapGetters獲取Modules中的Getters方式

    這篇文章主要介紹了VueX?mapGetters獲取Modules中的Getters方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • vue界面發(fā)送表情的實現(xiàn)代碼

    vue界面發(fā)送表情的實現(xiàn)代碼

    這篇文章主要介紹了vue界面發(fā)送表情的實現(xiàn)代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • vue+three.js實現(xiàn)炫酷的3D登陸頁面示例詳解

    vue+three.js實現(xiàn)炫酷的3D登陸頁面示例詳解

    這篇文章主要為大家介紹了vue+three.js實現(xiàn)炫酷的3D登陸頁面示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • Vue路由組件傳遞參數(shù)的六種場景

    Vue路由組件傳遞參數(shù)的六種場景

    在Vue應用程序中,路由組件是構建單頁應用的關鍵部分,傳遞參數(shù)給路由組件可以讓我們動態(tài)地展示內(nèi)容,處理用戶輸入,以及實現(xiàn)各種交互功能,本文就給大家介紹了六種Vue路由組件傳遞參數(shù)場景,需要的朋友可以參考下
    2023-09-09
  • vue better scroll 無法滾動的解決方法

    vue better scroll 無法滾動的解決方法

    better scroll可以實現(xiàn)輪播圖和頁面滾動,是移動端滾動插件,這篇文章主要介紹了vue better scroll 無法滾動的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-06-06
  • Vue模仿實現(xiàn)京東商品大圖放大鏡效果

    Vue模仿實現(xiàn)京東商品大圖放大鏡效果

    這篇文章主要為大家介紹了Vue實現(xiàn)京東網(wǎng)站商品放大鏡效果示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • el-select 數(shù)據(jù)回顯,只顯示value不顯示lable問題

    el-select 數(shù)據(jù)回顯,只顯示value不顯示lable問題

    這篇文章主要介紹了el-select 數(shù)據(jù)回顯,只顯示value不顯示lable問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-09-09
  • Vue如何動態(tài)改變列表搜索出關鍵詞的字體顏色

    Vue如何動態(tài)改變列表搜索出關鍵詞的字體顏色

    這篇文章主要介紹了Vue如何動態(tài)改變列表搜索出關鍵詞的字體顏色問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • 詳解vue-cli 構建Vue項目遇到的坑

    詳解vue-cli 構建Vue項目遇到的坑

    本篇文章主要介紹了詳解vue-cli 構建Vue項目遇到的坑,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • vue中使用element組件時事件想要傳遞其他參數(shù)的問題

    vue中使用element組件時事件想要傳遞其他參數(shù)的問題

    這篇文章主要介紹了vue中使用element組件時事件想要傳遞其他參數(shù)的問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-09-09

最新評論