突破Hooks全部限制,只要50行代碼

你們好,我是卡頌。html

你是否很討厭Hooks調用順序的限制(Hooks不能寫在條件語句裏)?前端

你是否遇到過在useEffect中使用了某個state,又忘記將其加入依賴項,致使useEffect回調執行時機出問題?react

怪本身粗心?怪本身很差好看文檔?數組

答應我,不要怪本身。markdown

根本緣由在於React沒有將Hooks實現爲響應式更新。數據結構

是實現難度很高麼?本文會用50行代碼實現無限制版Hooks,其中涉及的知識也是VueMobx等基於響應式更新的庫的底層原理。框架

本文的正確食用方式是收藏後用電腦看,跟着我一塊兒敲代碼(完整在線Demo連接見文章結尾)。函數

手機黨要是看了懵逼的話不要自責,是你食用方式不對。oop

注:本文代碼來自Ryan Carniato的文章Building a Reactive Library from Scratch,老哥是SolidJS做者ui

萬丈高樓平地起

首先來實現useState

function useState(value) {
  const getter = () => value;
  const setter = (newValue) => value = newValue;
  
  return [getter, setter];
}
複製代碼

返回值數組第一項負責取值,第二項負責賦值。相比React,咱們有個小改動:返回值的第一個參數是個函數而不是state自己。

使用方式以下:

const [count, setCount] = useState(0);

console.log(count()); // 0
setCount(1);
console.log(count()); // 1
複製代碼

沒有黑魔法

接下來實現useEffect,包括幾個要點:

  • 依賴的state改變,useEffect回調執行

  • 不須要顯式的指定依賴項(即ReactuseEffect的第二個參數)

舉個例子:

const [count, setCount] = useState(0);

useEffect(() => {
  window.title = count();
})
useEffect(() => {
  console.log('沒我啥事兒')
})
複製代碼

count變化後第一個useEffect會執行回調(由於他內部依賴count),可是第二個useEffect不會執行。

前端沒有黑魔法,這裏是如何實現的呢?

magic.gif

答案是:訂閱發佈。

繼續用上面的例子來解釋訂閱發佈關係創建的時機:

const [count, setCount] = useState(0);

useEffect(() => {
  window.title = count();
})
複製代碼

useEffect定義後他的回調會馬上執行一次,在其內部會執行:

window.title = count();
複製代碼

count執行時會創建effectstate之間訂閱發佈的關係。

當下次執行setCount(setter)時會通知訂閱了count變化的useEffect,執行其回調函數。

數據結構之間的關係如圖:

每一個useState內部有個集合subs,用來保存訂閱該state變化effect

effect是每一個useEffect對應的數據結構:

const effect = {
  execute,
  deps: new Set()
}
複製代碼

其中:

  • execute:該useEffect的回調函數

  • deps:該useEffect依賴的state對應subs的集合

我知道你有點暈。看看上面的結構圖,緩緩,咱再繼續。

實現useEffect

首先須要一個棧來保存當前正在執行的effect。這樣當調用getterstate才知道應該與哪一個effect創建聯繫。

舉個例子:

// effect1
useEffect(() => {
  window.title = count();
})
// effect2
useEffect(() => {
  console.log('沒我啥事兒')
})
複製代碼

count執行時須要知道本身處在effect1的上下文中(而不是effect2),這樣才能與effect1創建聯繫。

// 當前正在執行effect的棧
const effectStack = [];
複製代碼

接下來實現useEffect,包括以下功能點:

  • 每次useEffect回調執行前重置依賴(回調內部stategetter會重建依賴關係)

  • 回調執行時確保當前effect處在effectStack棧頂

  • 回調執行後將當前effect從棧頂彈出

代碼以下:

function useEffect(callback) {
    const execute = () => {
      // 重置依賴
      cleanup(effect);
      // 推入棧頂
      effectStack.push(effect);

      try {
        callback();
      } finally {
        // 出棧
        effectStack.pop();
      }
    }
    const effect = {
      execute,
      deps: new Set()
    }
    // 馬上執行一次,創建依賴關係
    execute();
  }
複製代碼

cleanup用來移除該effect與全部他依賴的state之間的聯繫,包括:

  • 訂閱關係:將該effect訂閱的全部state變化移除

  • 依賴關係:將該effect依賴的全部state移除

function cleanup(effect) {
  // 將該effect訂閱的全部state變化移除
  for (const dep of effect.deps) {
    dep.delete(effect);
  }
  // 將該effect依賴的全部state移除
  effect.deps.clear();
}
複製代碼

移除後,執行useEffect回調會再逐一重建關係。

改造useState

接下來改造useState,完成創建訂閱發佈關係的邏輯,要點以下:

  • 調用getter時獲取當前上下文的effect,創建關係

  • 調用setter時通知全部訂閱該state變化的effect回調執行

function useState(value) {
  // 訂閱列表
  const subs = new Set();

  const getter = () => {
    // 獲取當前上下文的effect
    const effect = effectStack[effectStack.length - 1];
    if (effect) {
      // 創建聯繫
      subscribe(effect, subs);
    }
    return value;
  }
  const setter = (nextValue) => {
    value = nextValue;
    // 通知全部訂閱該state變化的effect回調執行
    for (const sub of [...subs]) {
      sub.execute();
    }
  }
  return [getter, setter];
}
複製代碼

subscribe的實現,一樣包括2個關係的創建:

function subscribe(effect, subs) {
  // 訂閱關係創建
  subs.add(effect);
  // 依賴關係創建
  effect.deps.add(subs);
}
複製代碼

讓咱們來試驗下:

const [name1, setName1] = useState('KaSong');
useEffect(() => console.log('誰在那兒!', name1())) 
// 打印: 誰在那兒! KaSong
setName1('KaKaSong');
// 打印: 誰在那兒! KaKaSong
複製代碼

實現useMemo

接下來基於已有的2個hook實現useMemo

function useMemo(callback) {
  const [s, set] = useState();
  useEffect(() => set(callback()));
  return s;
}
複製代碼

自動依賴跟蹤

這套50行的Hooks還有個強大的隱藏特性:自動依賴跟蹤。

咱們拓展下上面的例子:

const [name1, setName1] = useState('KaSong');
const [name2, setName2] = useState('XiaoMing');
const [showAll, triggerShowAll] = useState(true);

const whoIsHere = useMemo(() => {
  if (!showAll()) {
    return name1();
  }
  return `${name1()}${name2()}`;
})

useEffect(() => console.log('誰在那兒!', whoIsHere()))
複製代碼

如今咱們有3個statename1name2showAll

whoIsHere做爲memo,依賴以上三個state

最後,當whoIsHere變化時,會觸發useEffect回調。

當以上代碼運行後,基於初始的3個state,會計算出whoIsHere,進而觸發useEffect回調,打印:

// 打印:誰在那兒! KaSong 和 XiaoMing
複製代碼

接下來調用:

setName1('KaKaSong');
// 打印:誰在那兒! KaKaSong 和 XiaoMing
triggerShowAll(false);
// 打印:誰在那兒! KaKaSong
複製代碼

下面的事情就有趣了,當調用:

setName2('XiaoHong');
複製代碼

並無log打印。

這是由於當triggerShowAll(false)致使showAll statefalse後,whoIsHere進入以下邏輯:

if (!showAll()) {
  return name1();
}
複製代碼

因爲沒有執行name2,因此name2whoIsHere已經沒有訂閱發佈關係了!

只有當triggerShowAll(true)後,whoIsHere進入以下邏輯:

return `${name1()}${name2()}`;
複製代碼

此時whoIsHere纔會從新依賴name1name2

自動的依賴跟蹤,是否是很酷~

總結

至此,基於訂閱發佈,咱們實現了能夠自動依賴跟蹤的無限制Hooks

這套理念是最近幾年纔有人使用麼?

早在2010年初KnockoutJS就用這種細粒度的方式實現響應式更新了。

不知道那時候,Steve SandersonKnockoutJS做者)有沒有預見到10年後的今天,細粒度更新會在各類庫和框架中被普遍使用。

這裏是:完整在線Demo連接

相關文章
相關標籤/搜索