React函數(shù)組件hook原理及構(gòu)建hook鏈表算法詳情
寫在前面的小結(jié)
- 每一個 hook 函數(shù)都有對應(yīng)的 hook 對象保存狀態(tài)信息
useContext
是唯一一個不需要添加到 hook 鏈表的 hook 函數(shù)- 只有 useEffect、useLayoutEffect 以及 useImperativeHandle 這三個 hook 具有副作用,在 render 階段需要給函數(shù)組件 fiber 添加對應(yīng)的副作用標(biāo)記。同時這三個 hook 都有對應(yīng)的 effect 對象保存其狀態(tài)信息
- 每次渲染都是重新構(gòu)建 hook 鏈表以及 收集 effect list(fiber.updateQueue)
- 初次渲染調(diào)用 mountWorkInProgressHook 構(gòu)建 hook 鏈表。更新渲染調(diào)用 updateWorkInProgressHook 構(gòu)建 hook 鏈表并復(fù)用上一次的 hook 狀態(tài)信息
Demo
可以用下面的 demo 在本地調(diào)試
import React, { useState, useEffect, useContext, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, forwardRef, } from "react"; import ReactDOM from "react-dom"; const themes = { foreground: "red", background: "#eeeeee", }; const ThemeContext = React.createContext(themes); const Home = forwardRef((props, ref) => { debugger; const [count, setCount] = useState(0); const myRef = useRef(null); const theme = useContext(ThemeContext); useEffect(() => { console.log("useEffect", count); }, [count]); useLayoutEffect(() => { console.log("useLayoutEffect...", myRef); }); const res = useMemo(() => { console.log("useMemo"); return count * count; }, [count]); console.log("res...", res); useImperativeHandle(ref, () => ({ focus: () => { myRef.current.focus(); }, })); const onClick = useCallback(() => { setCount(count + 1); }, [count]); return ( <div style={{ color: theme.foreground }} ref={myRef} onClick={onClick}> {count} </div> ); }); ReactDOM.render(<Home />, document.getElementById("root"));
fiber
React 在初次渲染或者更新過程中,都會在 render 階段創(chuàng)建新的或者復(fù)用舊的 fiber 節(jié)點。每一個函數(shù)組件,都有對應(yīng)的 fiber 節(jié)點。
fiber 的主要屬性如下:
var fiber = { alternate, child, elementType: () => {}, memoizedProps: null, memoizedState: null, // 在函數(shù)組件中,memoizedState用于保存hook鏈表 pendingProps: {}, return, sibling, stateNode, tag, // fiber的類型,函數(shù)組件對應(yīng)的tag為2 type: () => {} updateQueue: null, }
在函數(shù)組件的 fiber 中,有兩個屬性和 hook 有關(guān):memoizedState
和updateQueue
屬性。
- memoizedState 屬性用于保存 hook 鏈表,hook 鏈表是單向鏈表。
- updateQueue 屬性用于收集hook的副作用信息,保存
useEffect
、useLayoutEffect
、useImperativeHandle
這三個 hook 的 effect 信息,是一個環(huán)狀鏈表,其中 updateQueue.lastEffect 指向最后一個 effect 對象。effect 描述了 hook 的信息,比如useLayoutEffect
的 effect 對象保存了監(jiān)聽函數(shù),清除函數(shù),依賴等。
hook 鏈表
React 為我們提供的以use
開頭的函數(shù)就是 hook,本質(zhì)上函數(shù)在執(zhí)行完成后,就會被銷毀,然后狀態(tài)丟失。React 能記住這些函數(shù)的狀態(tài)信息的根本原因是,在函數(shù)組件執(zhí)行過程中,React 會為每個 hook 函數(shù)創(chuàng)建對應(yīng)的 hook 對象,然后將狀態(tài)信息保存在 hook 對象中,在下一次更新渲染時,會從這些 hook 對象中獲取上一次的狀態(tài)信息。
在函數(shù)組件執(zhí)行的過程中,比如上例中,當(dāng)執(zhí)行 Home()
函數(shù)組件時,React 會為組件內(nèi)每個 hook 函數(shù)創(chuàng)建對應(yīng)的 hook 對象,這些 hook 對象保存 hook 函數(shù)的信息以及狀態(tài),然后將這些 hook 對象連成一個鏈表。上例中,第一個執(zhí)行的是useState
hook,React 為其創(chuàng)建一個 hook:stateHook。第二個執(zhí)行的是useRef
hook,同樣為其創(chuàng)建一個 hook:refHook,然后將 stateHook.next 指向 refHook:stateHook.next = refHook。同理,refHook.next = effectHook,...
需要注意:
useContext
是唯一一個不會出現(xiàn)在 hook 鏈表中的 hook。- useState 是 useReducer 的語法糖,因此這里只需要用 useState 舉例就好。
useEffect
、useLayoutEffect
、useImperativeHandle
這三個 hook 都是屬于 effect 類型的 hook,他們的 effect 對象都需要被添加到函數(shù)組件 fiber 的 updateQueue 中,以便在 commit 階段執(zhí)行。
上例中,hook 鏈表如下紅色虛線中所示:
hook 對象及其屬性介紹
函數(shù)組件內(nèi)部的每一個 hook 函數(shù),都有對應(yīng)的 hook 對象用來保存 hook 函數(shù)的狀態(tài)信息,hook 對象的屬性如下:
var hook = { memoizedState,, baseState, baseQueue, queue, next, };
注意,hook 對象中的memoizedState
屬性和 fiber 的memoizedState
屬性含義不同。next
指向下一個 hook 對象,函數(shù)組件中的 hook 就是通過 next 指針連成鏈表
同時,不同的 hook 中,memoizedState 的含義不同,下面詳細(xì)介紹各類型 hook 對象的屬性含義
useState Hook 對象
- hook.memoizedState 保存的是 useState 的 state 值。比如
const [count, setCount] = useState(0)
中,memoizedState 保存的就是 state 的值。 - hook.queue 保存的是更新隊列,是個環(huán)狀鏈表。queue 的屬性如下:
hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState, };
比如我們在 onClick 中多次調(diào)用setCount
:
const onClick = useCallback(() => { debugger; setCount(count + 1); setCount(2); setCount(3); }, [count]);
每次調(diào)用setCount
,都會創(chuàng)建一個新的 update 對象,并添加進 hook.queue 中,update 對象屬性如下:
var update = { lane: lane, action: action, // setCount的參數(shù) eagerReducer: null, eagerState: null, next: null, };
queue.pending 指向最后一個更新對象。
queue 隊列如下紅色實線所示:
在 render 階段,會遍歷 hook.queue,計算最終的 state 值,并存入 hook.memoizedState 中
useRef Hook
- hook.memoizedState 保存的是 ref 的值。
比如:
const myRef = useRef(null);
那么 memoizedState 保存的是 myRef 的值,即:
hook.memoizedState = { current, };
useEffect、useLayoutEffect 以及 useImperativeHandle
- memoizedState 保存的是一個 effect 對象,effect 對象保存的是 hook 的狀態(tài)信息,比如監(jiān)聽函數(shù),依賴,清除函數(shù)等,
屬性如下:
var effect = { tag: tag, // effect的類型,useEffect對應(yīng)的tag為5,useLayoutEffect對應(yīng)的tag為3 create: create, // useEffect或者useLayoutEffect的監(jiān)聽函數(shù),即第一個參數(shù) destroy: destroy, // useEffect或者useLayoutEffect的清除函數(shù),即監(jiān)聽函數(shù)的返回值 deps: deps, // useEffect或者useLayoutEffect的依賴,第二個參數(shù) // Circular next: null, // 在updateQueue中使用,將所有的effect連成一個鏈表 };
這三個 hook 都屬于 effect 類型的 hook,即具有副作用的 hook
- useEffect 的副作用為:Update | Passive,即 516
- useLayoutEffect 和 useImperativeHandle 的副作用都是:Update,即 4
在函數(shù)組件中,也就只有這三個 hook 才具有副作用,在 hook 執(zhí)行的過程中需要給 fiber 添加對應(yīng)的副作用標(biāo)記。然后在 commit 階段執(zhí)行對應(yīng)的操作,比如調(diào)用useEffect
的監(jiān)聽函數(shù),清除函數(shù)等等。
因此,React 需要將這三個 hook 函數(shù)的 effect 對象存到 fiber.updateQueue 中,以便在 commit 階段遍歷 updateQueue,執(zhí)行對應(yīng)的操作。updateQueue 也是一個環(huán)狀鏈表,lastEffect 指向最后一個 effect 對象。effect 和 effect 之間通過 next 相連。
const effect = { create: () => { console.log("useEffect", count); }, deps: [0] destroy: undefined, tag: 5, } effect.next = effect fiber.updateQueue = { lastEffect: effect, };
fiber.updateQueue 如下圖紅色實線所示:
hook 對應(yīng)的 effect 對象如下圖紅色實線所示:
useMemo
- hook.memoizedState 保存的是 useMemo 的值和依賴。比如:
const res = useMemo(() => { return count * count; }, [count]);
那么 memoizedState 保存的是返回值以及依賴,即:
hook.memoizedState = [count * count, [count]];
useCallback
hook.memoizedState 保存的是回調(diào)函數(shù)和依賴,比如:
const onClick = useCallback(callback dep);
那么 memoizedState=[callback, dep]
構(gòu)建 Hook 鏈表的源碼
React 在初次渲染和更新這兩個過程,構(gòu)建 hook 鏈表的算法不一樣,因此 React 對這兩個過程是分開處理的:
var HooksDispatcherOnMount = { useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useRef: mountRef, useState: mountState, }; var HooksDispatcherOnUpdate = { useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useRef: updateRef, useState: updateState, };
如果是初次渲染,則使用HooksDispatcherOnMount
,此時如果我們調(diào)用 useState,實際上調(diào)用的是HooksDispatcherOnMount.useState
,執(zhí)行的是mountState
方法。
如果是更新階段,則使用HooksDispatcherOnUpdate
,此時如果我們調(diào)用 useState,實際上調(diào)用的是HooksDispatcherOnUpdate.useState
,執(zhí)行的是updateState
初次渲染和更新渲染執(zhí)行 hook 函數(shù)的區(qū)別在于:
- 構(gòu)建 hook 鏈表的算法不同。初次渲染只是簡單的構(gòu)建 hook 鏈表。而更新渲染會遍歷上一次的 hook 鏈表,構(gòu)建新的 hook 鏈表,并復(fù)用上一次的 hook 狀態(tài)
- 依賴的判斷。初次渲染不需要判斷依賴。更新渲染需要判斷依賴是否變化。
- 對于 useState 來說,更新階段還需要遍歷 queue 鏈表,計算最新的狀態(tài)。
renderWithHooks 函數(shù)組件執(zhí)行
不管是初次渲染還是更新渲染,函數(shù)組件的執(zhí)行都是從renderWithHooks
函數(shù)開始執(zhí)行。
function renderWithHooks(current, workInProgress, Component, props) { currentlyRenderingFiber = workInProgress; workInProgress.memoizedState = null; workInProgress.updateQueue = null; ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; var children = Component(props, secondArg); currentlyRenderingFiber = null; currentHook = null; workInProgressHook = null; return children; }
renderWithHooks 的Component
參數(shù)就是我們的函數(shù)組件,在本例中,就是Home
函數(shù)。
Component 開始執(zhí)行前,會重置 memoizedState 和 updateQueue 屬性,因此每次渲染都是重新構(gòu)建 hook 鏈表以及收集 effect list
renderWithHooks 方法初始化以下全局變量
- currentlyRenderingFiber。fiber 節(jié)點。當(dāng)前正在執(zhí)行的函數(shù)組件對應(yīng)的 fiber 節(jié)點,這里是 Home 組件的 fiber 節(jié)點
- ReactCurrentDispatcher.current。負(fù)責(zé)派發(fā) hook 函數(shù),初次渲染時,指向 HooksDispatcherOnMount,更新渲染時指向 HooksDispatcherOnUpdate。
比如我們在函數(shù)組件內(nèi)部調(diào)用 useState,實際上調(diào)用的是:
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function resolveDispatcher() { var dispatcher = ReactCurrentDispatcher.current; return dispatcher; }
每一個 hook 函數(shù)在執(zhí)行時,都會調(diào)用resolveDispatcher
方法獲取當(dāng)前的dispatcher
,然后調(diào)用dispatcher
中對應(yīng)的方法處理 mount 或者 update 邏輯。
以 useEffect 為例,在初次渲染時調(diào)用的是:
function mountEffectImpl(fiberFlags, hookFlags, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HasEffect | hookFlags, create, undefined, nextDeps ); }
在更新渲染時,調(diào)用的是
function updateEffectImpl(fiberFlags, hookFlags, create, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var destroy = undefined; if (currentHook !== null) { var prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { var prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HasEffect | hookFlags, create, destroy, nextDeps ); }
pushEffect 方法構(gòu)建一個 effect 對象并添加到 fiber.updateQueue 中,同時返回 effect 對象。
mountEffectImpl 方法邏輯比較簡單,而 updateEffectImpl 方法還多了一個判斷依賴是否變化的邏輯。
mountWorkInProgressHook
以及updateWorkInProgressHook
方法用來在函數(shù)組件執(zhí)行過程中構(gòu)建 hook 鏈表,這也是構(gòu)建 hook 鏈表的算法。每一個 hook 函數(shù)在執(zhí)行的過程中都會調(diào)用這兩個方法
構(gòu)建 hook 鏈表的算法
初次渲染和更新渲染,構(gòu)建 hook 鏈表的算法不同。初次渲染使用mountWorkInProgressHook
,而更新渲染使用updateWorkInProgressHook
。
- mountWorkInProgressHook 直接為每個 hook 函數(shù)創(chuàng)建對應(yīng)的 hook 對象
- updateWorkInProgressHook 在執(zhí)行每個 hook 函數(shù)時,同時遍歷上一次的 hook 鏈表,以復(fù)用上一次 hook 的狀態(tài)信息。這個算法稍稍復(fù)雜
React 使用全局變量workInProgressHook
保存當(dāng)前正在執(zhí)行的 hook 對象。比如,本例中,第一個執(zhí)行的是useState
,則此時workInProgressHook=stateHook
。第二個執(zhí)行的是useRef
,則此時workInProgressHook=refHook
,...。
可以將 workInProgressHook
看作鏈表的指針
mountWorkInProgressHook 構(gòu)建 hook 鏈表算法
代碼如下:
function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // hook鏈表中的第一個hook currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // 添加到hook鏈表末尾 workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
可以看出,初次渲染構(gòu)建 hook 鏈表的算法邏輯非常簡單,為每一個 hook 函數(shù)創(chuàng)建對應(yīng)的 hook 對象,然后添加到 hook 鏈表末尾就行
updateWorkInProgressHook 構(gòu)建 hook 鏈表算法
更新渲染階段構(gòu)建 hook 鏈表的算法就比較麻煩。我們從 fiber 開始
我們知道 React 在 render 階段會復(fù)用 fiber 節(jié)點,假設(shè)我們第一次渲染完成的 fiber 節(jié)點如下:
var firstFiber = { ..., // 省略其他屬性 alternate: null, // 由于是第一次渲染,alternate為null memoizedState, // 第一次渲染構(gòu)建的hook鏈表 updateQueue, // 第一次渲染收集的effect list };
經(jīng)過第一次渲染以后,我們將得到下面的 hook 鏈表:
當(dāng)我們點擊按鈕觸發(fā)更新,renderWithHooks 函數(shù)開始調(diào)用,但 Home 函數(shù)執(zhí)行前,此時workInProgressHook
、currentHook
都為 null。同時新的 fiber 的memoizedState
、updateQueue
都被重置為 null
workInProgressHook
用于構(gòu)建新的 hook 鏈表
currentHook
用于遍歷上一次渲染構(gòu)建的 hook 鏈表,即舊的鏈表,或者當(dāng)前的鏈表(即和當(dāng)前顯示的頁面對應(yīng)的 hook 鏈表)
按照本例中調(diào)用 hook 函數(shù)的順序,一步步拆解updateWorkInProgressHook
算法的過程
- 第一步 調(diào)用 useState
由于此時 currentHook
為 null,因此我們需要初始化它指向舊的 hook 鏈表的第一個 hook 對象。
if (currentHook === null) { var current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } currentHook = nextCurrentHook;
創(chuàng)建一個新的 hook 對象,復(fù)用上一次的 hook 對象的狀態(tài)信息,并初始化 hook 鏈表
var newHook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, // 注意,next被重置了!!!!! }; if (workInProgressHook === null) { currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; }
- 第二步 調(diào)用 useRef
此時 currentHook 已經(jīng)有值,指向第一個 hook 對象。因此將 currentHook 指向它的下一個 hook 對象,即第二個
if (currentHook === null) { } else { nextCurrentHook = currentHook.next; } currentHook = nextCurrentHook;
同樣的,也需要為 useRef 創(chuàng)建一個新的 hook 對象,并復(fù)用上一次的 hook 狀態(tài)
后面的 hook 的執(zhí)行過程和 useRef 一樣,都是一邊遍歷舊的 hook 鏈表,為當(dāng)前 hook 函數(shù)創(chuàng)建新的 hook 對象,然后復(fù)用舊的 hook 對象的狀態(tài)信息,然后添加到 hook 鏈表中
從更新渲染的過程也可以看出,hook 函數(shù)的執(zhí)行是會遍歷舊的 hook 鏈表并復(fù)用舊的 hook 對象的狀態(tài)信息。這也是為什么我們不能將 hook 函數(shù)寫在條件語句或者循環(huán)中的根本原因,我們必須保證 hook 函數(shù)的順序在任何時候都要一致
完整源碼
最終完整的算法如下:
function updateWorkInProgressHook() { var nextCurrentHook; if (currentHook === null) { var current = currentlyRenderingFiber$1.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { nextCurrentHook = currentHook.next; } var nextWorkInProgressHook; if (workInProgressHook === null) { nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState; } else { nextWorkInProgressHook = workInProgressHook.next; } if (nextWorkInProgressHook !== null) { // There's already a work-in-progress. Reuse it. workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; } else { // Clone from the current hook. if (!(nextCurrentHook !== null)) { { throw Error(formatProdErrorMessage(310)); } } currentHook = nextCurrentHook; var newHook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list. currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook; } else { // Append to the end of the list. workInProgressHook = workInProgressHook.next = newHook; } } return workInProgressHook; }
到此這篇關(guān)于React函數(shù)組件hook原理及構(gòu)建hook鏈表算法詳情的文章就介紹到這了,更多相關(guān)React函數(shù)組件hook內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ReactNative頁面跳轉(zhuǎn)Navigator實現(xiàn)的示例代碼
本篇文章主要介紹了ReactNative頁面跳轉(zhuǎn)Navigator實現(xiàn)的示例代碼,具有一定的參考價值,有興趣的可以了解一下2017-08-08React?Hook?Form?優(yōu)雅處理表單使用指南
這篇文章主要為大家介紹了React?Hook?Form?優(yōu)雅處理表單使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03一文詳解手動實現(xiàn)Recoil狀態(tài)管理基本原理
這篇文章主要為大家介紹了一文詳解手動實現(xiàn)Recoil狀態(tài)管理基本原理實例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05