React Hooks 的原理,有的簡單有的不簡單

React 是實現了組件的前端框架,它支持 class 和 function 兩種形式的組件。

class 組件是通過繼承模版類(Component、PureComponent)的方式開發新組件的,繼承是 class 本身的特性,它支持設置 state,會在 state 改變後重新渲染,可以重寫一些父類的方法,這些方法會在 React 組件渲染的不同階段調用,叫做生命週期函數。

function 組件不能做繼承,因爲 function 本來就沒這個特性,所以是提供了一些 api 供函數使用,這些 api 會在內部的一個數據結構上掛載一些函數和值,並執行相應的邏輯,通過這種方式實現了 state 和類似 class 組件的生命週期函數的功能,這種 api 就叫做 hooks。

hooks 掛載數據的數據結構叫做 fiber。

那什麼是 fiber 呢?

我們知道,React 是通過 jsx 來描述界面結構的,會把 jsx 編譯成 render function,然後執行 render function 產生 vdom:

在 v16 之前的 React 裏,是直接遞歸遍歷 vdom,通過 dom api 增刪改 dom 的方式來渲染的。但當 vdom 過大,頻繁調用 dom api 會比較耗時,而且遞歸又不能打斷,所以有性能問題。

後來就引入了 fiber 架構,先把 vdom 樹轉成 fiber 鏈表,然後再渲染 fiber。

vdom 轉 fiber 的過程叫做 reconcile,是可打斷的,React 加入了 schedule 的機制在空閒時調度 reconcile,reconcile 的過程中會做 diff,打上增刪改的標記(effectTag),並把對應的 dom 創建好。然後就可以一次性把 fiber 渲染到 dom,也就是 commit。

這個 schdule、reconcile、commit 的流程就是 fiber 架構。當然,對應的這個數據結構也叫 fiber。

(更多 fiber 的介紹可以看我之前的一篇文章:手寫簡易版 React 來徹底搞懂 fiber 架構

hooks 就是通過把數據掛載到組件對應的 fiber 節點上來實現的。

fiber 節點是一個對象,hooks 把數據掛載在哪個屬性呢?

我們可以 debugger 看下。

準備這樣一個函數組件(代碼沒啥具體含義,就是爲了調試 hooks):

function App() {
  const [name, setName] = useState("guang");
  useState('dong');

  const handler = useCallback((evt) ={
      setName('dong');
  },[1]);

  useEffect(() ={
    console.log(1);
  });
  
  useRef(1);

  useMemo(() ={
    return 'guang and dong';
  })

  return (
    <div class>
      <header class>
        <img src={logo} class />
        <p onClick={handler}>
          {name}
        </p>
      </header>
    </div>
  );
}

在函數打個斷點,運行到這個組件就會斷住。

我們看下調用棧:

上一個函數是 renderWithHooks,裏面有個 workingInProgress 的對象就是當前的 fiber 節點:

fiber 節點的 memorizedState 就是保存 hooks 數據的地方。

它是一個通過 next 串聯的鏈表,展開看一下:

鏈表一共六個元素,這和我們在 function 組件寫的 hooks 不就對上了麼:

這就是 hooks 存取數據的地方,執行的時候各自在自己的那個 memorizedState 上存取數據,完成各種邏輯,這就是 hooks 的原理。

這個 memorizedState 鏈表是什麼時候創建的呢?

好問題,確實有個鏈表創建的過程,也就是 mountXxx。鏈表只需要創建一次,後面只需要 update。

所以第一次調用 useState 會執行 mountState,後面再調用 useState 會執行 updateState。

我們先集中精力把 mount 搞明白。

mountXxx 是創建 memorizedState 鏈表的過程,每個 hooks api 都是這樣的:

它的實現也很容易想到,就是創建對應的 memorizedState 對象,然後用 next 串聯起來,也就是這段代碼:

當然,創建這樣的數據結構還是爲了使用的,每種 hooks api 都有不同的使用這些 memorizedState 數據的邏輯,有的比較簡單,比如 useRef、useCallback、useMemo,有的沒那麼簡單,比如 useState、useEffect。

爲什麼這麼說呢?我們看下它們的實現再說吧。

先看這幾個簡單的:

useRef

每個 useXxx 的 hooks 都有 mountXxx 和 updateXxx 兩個階段,比如 ref 就是 mountRef 和 updateRef。

它的代碼是最簡單的,只有這麼幾行:

mountWorkInProgressHook 剛纔我們看過,就是創建並返回 memorizedState 鏈表的,同理,下面那個 updateWorkInProgressHook 是更新的。

這些不用管,只要知道修改的是對應的 memorizedState 鏈表中的元素就行了。

那 ref 在 memorizedState 上掛了什麼呢?

可以看到是把傳進來的 value 包裝了一個有 current 屬性的對象,凍結了一下,然後放在 memorizedState 屬性上。

後面 update 的時候,沒有做任何處理,直接返回這個對象。

所以,useRef 的功能就很容易猜到了:useRef 可以保存一個數據的引用,這個引用不可變。

這個 hooks 是最簡單的 hooks 了,給我們一個地方存數據,我們也能輕易的實現 useRef 這個 hooks。

再來看個稍難點的:

useCallback

useCallback 在 memorizedState 上放了一個數組,第一個元素是傳入的回調函數,第二個是傳入的 deps(對 deps 做了下 undefined 的處理)。

更新的時候把之前的那個 memorizedState 取出來,和新傳入的 deps 做下對比,如果沒變,那就返回之前的回調函數,也就是 prevState[0]。

如果變了,那就創建一個新的數組,第一個元素是傳入的回調函數,第二個是傳入的 deps。

所以,useCallback 的功能也就呼之欲出了:useCallback 可以實現函數的緩存,如果 deps 沒變就不會創建新的,否則纔會返回新傳入的函數。

這段邏輯其實也不難,就是多了個判斷邏輯。

再來看個和它差不多的:

useMemo

useMemo 也在 memorizedState 上放了個數組,第一個元素是傳入函數的執行結果,第二個元素是 deps(對 deps 爲 undefined 的情況做了下處理)。

更新的時候也是取出之前的 memorizedState,和新傳入的 deps 做下對比,如果沒變,就返回之前的值,也就是 prevState[0]。

如果變了,創建一個新的數組放在 memorizedState,第一個元素是新傳入函數的執行結果,第二個元素是 deps。

所以,useMemo 的功能大家也能猜出來:useMemo 可以實現函數執行結果的緩存,如果 deps 沒變,就直接拿之前的,否則纔會執行函數拿到最新結果返回。

實現邏輯和 useCallback 大同小異。

這三個 hooks 難麼?給大家一個對象來存儲數據,大家都能寫出來,並不難。

因爲它們是沒有別的依賴的,只是單純的緩存了下值而已。而像 useState、useEffect 這些就複雜一些了,主要是因爲需要調度。

useState

state 改了之後是要觸發更新的調度的,React 有自己的調度邏輯,就是我們前面提到的 fiber 的 schedule,所以需要 dispatch 一個 action。

(不展開講,簡單看一下)

這裏詳細講要涉及到調度,就先不展開了。

useEffect

同樣的,effect 傳入的函數也是被 React 所調度的,當然,這裏的調度不是 fiber 那個調度,而是單獨的 effect 調度:

(不展開講,簡單看一下)

hooks 負責把這些 effect 串聯成一個 updateQueue 的鏈表,然後讓 React 去調度執行。

所以說,useState、useEffect 這種 hooks 的實現是和 fiber 的空閒調度,effect 的調度結合比較緊密的,實現上更復雜了一些。

這裏沒有展開講,因爲這篇文章的目的是把 hooks 的主要原理理清楚,不會太深入細節。

大家可能還聽過自定義 hooks 的概念,那個是啥呢?

其實就是個函數調用,沒啥神奇的,我們可以把上面的 hooks 放到 xxx 函數里,然後在 function 組件裏調用,對應的 hook 鏈表是一樣的。

只不過一般我們會使用 React 提供的 eslint 插件,lint 了這些函數必須以 use 開頭,但其實不用也沒事,它們和普通的函數封裝沒有任何區別。

總結

React 支持 class 和 function 兩種形式的組件,class 支持 state 屬性和生命週期方法,而 function 組件也通過 hooks api 實現了類似的功能。

fiber 架構是 React 在 16 以後引入的,之前是 jsx -> render function -> vdom 然後直接遞歸渲染 vdom,現在則是多了一步 vdom 轉 fiber 的 reconcile,在 reconcile 的過程中創建 dom 和做 diff 並打上增刪改的 effectTag,然後一次性 commit。這個 reconcile 是可被打斷的,可以調度,也就是 fiber 的 schedule。

hooks 的實現就是基於 fiber 的,會在 fiber 節點上放一個鏈表,每個節點的 memorizedState 屬性上存放了對應的數據,然後不同的 hooks api 使用對應的數據來完成不同的功能。

鏈表自然有個創建階段,也就是 mountXxx,之後就不需要再 mount 了,只需要 update。所以每個 useXx 的實現其實都是分爲了 mountXxx 和 updateXxx 兩部分的。

我們看了幾個簡單的 hooks:useRef、useCallback、useMemo,它們只是對值做了緩存,邏輯比較純粹,沒有依賴 React 的調度。而 useState 會觸發 fiber 的 schedule,useEffect 也有自己的調度邏輯。實現上相對複雜一些,我們沒有繼續深入。

其實給我們一個對象來存取數據,實現 useRef、useCallback、useMemo 等 hooks 還是很簡單的。對於需要調度的,則複雜一些。

對於自定義的 hooks,那個就是個函數調用,沒有任何區別。(lint 的規則不想遵守可以忽略)

所有 hooks api 都是基於 fiber 節點上的 memorizedState 鏈表來存取數據並完成各自的邏輯的。

所以,hooks 的原理簡單麼?只能說有的簡單,有的不簡單。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/u89G-Uas0mzZQsiWT_4EZQ