React函數(shù)組件hook原理及構(gòu)建hook鏈表算法詳情
寫在前面的小結(jié)
- 每一個(gè) hook 函數(shù)都有對(duì)應(yīng)的 hook 對(duì)象保存狀態(tài)信息
useContext
是唯一一個(gè)不需要添加到 hook 鏈表的 hook 函數(shù)- 只有 useEffect、useLayoutEffect 以及 useImperativeHandle 這三個(gè) hook 具有副作用,在 render 階段需要給函數(shù)組件 fiber 添加對(duì)應(yīng)的副作用標(biāo)記。同時(shí)這三個(gè) hook 都有對(duì)應(yīng)的 effect 對(duì)象保存其狀態(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 在初次渲染或者更新過程中,都會(huì)在 render 階段創(chuàng)建新的或者復(fù)用舊的 fiber 節(jié)點(diǎn)。每一個(gè)函數(shù)組件,都有對(duì)應(yīng)的 fiber 節(jié)點(diǎn)。
fiber 的主要屬性如下:
var fiber = { alternate, child, elementType: () => {}, memoizedProps: null, memoizedState: null, // 在函數(shù)組件中,memoizedState用于保存hook鏈表 pendingProps: {}, return, sibling, stateNode, tag, // fiber的類型,函數(shù)組件對(duì)應(yīng)的tag為2 type: () => {} updateQueue: null, }
在函數(shù)組件的 fiber 中,有兩個(gè)屬性和 hook 有關(guān):memoizedState
和updateQueue
屬性。
- memoizedState 屬性用于保存 hook 鏈表,hook 鏈表是單向鏈表。
- updateQueue 屬性用于收集hook的副作用信息,保存
useEffect
、useLayoutEffect
、useImperativeHandle
這三個(gè) hook 的 effect 信息,是一個(gè)環(huán)狀鏈表,其中 updateQueue.lastEffect 指向最后一個(gè) effect 對(duì)象。effect 描述了 hook 的信息,比如useLayoutEffect
的 effect 對(duì)象保存了監(jiān)聽函數(shù),清除函數(shù),依賴等。
hook 鏈表
React 為我們提供的以use
開頭的函數(shù)就是 hook,本質(zhì)上函數(shù)在執(zhí)行完成后,就會(huì)被銷毀,然后狀態(tài)丟失。React 能記住這些函數(shù)的狀態(tài)信息的根本原因是,在函數(shù)組件執(zhí)行過程中,React 會(huì)為每個(gè) hook 函數(shù)創(chuàng)建對(duì)應(yīng)的 hook 對(duì)象,然后將狀態(tài)信息保存在 hook 對(duì)象中,在下一次更新渲染時(shí),會(huì)從這些 hook 對(duì)象中獲取上一次的狀態(tài)信息。
在函數(shù)組件執(zhí)行的過程中,比如上例中,當(dāng)執(zhí)行 Home()
函數(shù)組件時(shí),React 會(huì)為組件內(nèi)每個(gè) hook 函數(shù)創(chuàng)建對(duì)應(yīng)的 hook 對(duì)象,這些 hook 對(duì)象保存 hook 函數(shù)的信息以及狀態(tài),然后將這些 hook 對(duì)象連成一個(gè)鏈表。上例中,第一個(gè)執(zhí)行的是useState
hook,React 為其創(chuàng)建一個(gè) hook:stateHook。第二個(gè)執(zhí)行的是useRef
hook,同樣為其創(chuàng)建一個(gè) hook:refHook,然后將 stateHook.next 指向 refHook:stateHook.next = refHook。同理,refHook.next = effectHook,...
需要注意:
useContext
是唯一一個(gè)不會(huì)出現(xiàn)在 hook 鏈表中的 hook。- useState 是 useReducer 的語(yǔ)法糖,因此這里只需要用 useState 舉例就好。
useEffect
、useLayoutEffect
、useImperativeHandle
這三個(gè) hook 都是屬于 effect 類型的 hook,他們的 effect 對(duì)象都需要被添加到函數(shù)組件 fiber 的 updateQueue 中,以便在 commit 階段執(zhí)行。
上例中,hook 鏈表如下紅色虛線中所示:
hook 對(duì)象及其屬性介紹
函數(shù)組件內(nèi)部的每一個(gè) hook 函數(shù),都有對(duì)應(yīng)的 hook 對(duì)象用來保存 hook 函數(shù)的狀態(tài)信息,hook 對(duì)象的屬性如下:
var hook = { memoizedState,, baseState, baseQueue, queue, next, };
注意,hook 對(duì)象中的memoizedState
屬性和 fiber 的memoizedState
屬性含義不同。next
指向下一個(gè) hook 對(duì)象,函數(shù)組件中的 hook 就是通過 next 指針連成鏈表
同時(shí),不同的 hook 中,memoizedState 的含義不同,下面詳細(xì)介紹各類型 hook 對(duì)象的屬性含義
useState Hook 對(duì)象
- hook.memoizedState 保存的是 useState 的 state 值。比如
const [count, setCount] = useState(0)
中,memoizedState 保存的就是 state 的值。 - hook.queue 保存的是更新隊(duì)列,是個(gè)環(huán)狀鏈表。queue 的屬性如下:
hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState, };
比如我們?cè)?onClick 中多次調(diào)用setCount
:
const onClick = useCallback(() => { debugger; setCount(count + 1); setCount(2); setCount(3); }, [count]);
每次調(diào)用setCount
,都會(huì)創(chuàng)建一個(gè)新的 update 對(duì)象,并添加進(jìn) hook.queue 中,update 對(duì)象屬性如下:
var update = { lane: lane, action: action, // setCount的參數(shù) eagerReducer: null, eagerState: null, next: null, };
queue.pending 指向最后一個(gè)更新對(duì)象。
queue 隊(duì)列如下紅色實(shí)線所示:
在 render 階段,會(huì)遍歷 hook.queue,計(jì)算最終的 state 值,并存入 hook.memoizedState 中
useRef Hook
- hook.memoizedState 保存的是 ref 的值。
比如:
const myRef = useRef(null);
那么 memoizedState 保存的是 myRef 的值,即:
hook.memoizedState = { current, };
useEffect、useLayoutEffect 以及 useImperativeHandle
- memoizedState 保存的是一個(gè) effect 對(duì)象,effect 對(duì)象保存的是 hook 的狀態(tài)信息,比如監(jiān)聽函數(shù),依賴,清除函數(shù)等,
屬性如下:
var effect = { tag: tag, // effect的類型,useEffect對(duì)應(yīng)的tag為5,useLayoutEffect對(duì)應(yīng)的tag為3 create: create, // useEffect或者useLayoutEffect的監(jiān)聽函數(shù),即第一個(gè)參數(shù) destroy: destroy, // useEffect或者useLayoutEffect的清除函數(shù),即監(jiān)聽函數(shù)的返回值 deps: deps, // useEffect或者useLayoutEffect的依賴,第二個(gè)參數(shù) // Circular next: null, // 在updateQueue中使用,將所有的effect連成一個(gè)鏈表 };
這三個(gè) hook 都屬于 effect 類型的 hook,即具有副作用的 hook
- useEffect 的副作用為:Update | Passive,即 516
- useLayoutEffect 和 useImperativeHandle 的副作用都是:Update,即 4
在函數(shù)組件中,也就只有這三個(gè) hook 才具有副作用,在 hook 執(zhí)行的過程中需要給 fiber 添加對(duì)應(yīng)的副作用標(biāo)記。然后在 commit 階段執(zhí)行對(duì)應(yīng)的操作,比如調(diào)用useEffect
的監(jiān)聽函數(shù),清除函數(shù)等等。
因此,React 需要將這三個(gè) hook 函數(shù)的 effect 對(duì)象存到 fiber.updateQueue 中,以便在 commit 階段遍歷 updateQueue,執(zhí)行對(duì)應(yīng)的操作。updateQueue 也是一個(gè)環(huán)狀鏈表,lastEffect 指向最后一個(gè) effect 對(duì)象。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 如下圖紅色實(shí)線所示:
hook 對(duì)應(yīng)的 effect 對(duì)象如下圖紅色實(shí)線所示:
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è)過程,構(gòu)建 hook 鏈表的算法不一樣,因此 React 對(duì)這兩個(gè)過程是分開處理的:
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
,此時(shí)如果我們調(diào)用 useState,實(shí)際上調(diào)用的是HooksDispatcherOnMount.useState
,執(zhí)行的是mountState
方法。
如果是更新階段,則使用HooksDispatcherOnUpdate
,此時(shí)如果我們調(diào)用 useState,實(shí)際上調(diào)用的是HooksDispatcherOnUpdate.useState
,執(zhí)行的是updateState
初次渲染和更新渲染執(zhí)行 hook 函數(shù)的區(qū)別在于:
- 構(gòu)建 hook 鏈表的算法不同。初次渲染只是簡(jiǎn)單的構(gòu)建 hook 鏈表。而更新渲染會(huì)遍歷上一次的 hook 鏈表,構(gòu)建新的 hook 鏈表,并復(fù)用上一次的 hook 狀態(tài)
- 依賴的判斷。初次渲染不需要判斷依賴。更新渲染需要判斷依賴是否變化。
- 對(duì)于 useState 來說,更新階段還需要遍歷 queue 鏈表,計(jì)算最新的狀態(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í)行前,會(huì)重置 memoizedState 和 updateQueue 屬性,因此每次渲染都是重新構(gòu)建 hook 鏈表以及收集 effect list
renderWithHooks 方法初始化以下全局變量
- currentlyRenderingFiber。fiber 節(jié)點(diǎn)。當(dāng)前正在執(zhí)行的函數(shù)組件對(duì)應(yīng)的 fiber 節(jié)點(diǎn),這里是 Home 組件的 fiber 節(jié)點(diǎn)
- ReactCurrentDispatcher.current。負(fù)責(zé)派發(fā) hook 函數(shù),初次渲染時(shí),指向 HooksDispatcherOnMount,更新渲染時(shí)指向 HooksDispatcherOnUpdate。
比如我們?cè)诤瘮?shù)組件內(nèi)部調(diào)用 useState,實(shí)際上調(diào)用的是:
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function resolveDispatcher() { var dispatcher = ReactCurrentDispatcher.current; return dispatcher; }
每一個(gè) hook 函數(shù)在執(zhí)行時(shí),都會(huì)調(diào)用resolveDispatcher
方法獲取當(dāng)前的dispatcher
,然后調(diào)用dispatcher
中對(duì)應(yīng)的方法處理 mount 或者 update 邏輯。
以 useEffect 為例,在初次渲染時(shí)調(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 ); }
在更新渲染時(shí),調(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)建一個(gè) effect 對(duì)象并添加到 fiber.updateQueue 中,同時(shí)返回 effect 對(duì)象。
mountEffectImpl 方法邏輯比較簡(jiǎn)單,而 updateEffectImpl 方法還多了一個(gè)判斷依賴是否變化的邏輯。
mountWorkInProgressHook
以及updateWorkInProgressHook
方法用來在函數(shù)組件執(zhí)行過程中構(gòu)建 hook 鏈表,這也是構(gòu)建 hook 鏈表的算法。每一個(gè) hook 函數(shù)在執(zhí)行的過程中都會(huì)調(diào)用這兩個(gè)方法
構(gòu)建 hook 鏈表的算法
初次渲染和更新渲染,構(gòu)建 hook 鏈表的算法不同。初次渲染使用mountWorkInProgressHook
,而更新渲染使用updateWorkInProgressHook
。
- mountWorkInProgressHook 直接為每個(gè) hook 函數(shù)創(chuàng)建對(duì)應(yīng)的 hook 對(duì)象
- updateWorkInProgressHook 在執(zhí)行每個(gè) hook 函數(shù)時(shí),同時(shí)遍歷上一次的 hook 鏈表,以復(fù)用上一次 hook 的狀態(tài)信息。這個(gè)算法稍稍復(fù)雜
React 使用全局變量workInProgressHook
保存當(dāng)前正在執(zhí)行的 hook 對(duì)象。比如,本例中,第一個(gè)執(zhí)行的是useState
,則此時(shí)workInProgressHook=stateHook
。第二個(gè)執(zhí)行的是useRef
,則此時(shí)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鏈表中的第一個(gè)hook currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // 添加到hook鏈表末尾 workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
可以看出,初次渲染構(gòu)建 hook 鏈表的算法邏輯非常簡(jiǎn)單,為每一個(gè) hook 函數(shù)創(chuàng)建對(duì)應(yīng)的 hook 對(duì)象,然后添加到 hook 鏈表末尾就行
updateWorkInProgressHook 構(gòu)建 hook 鏈表算法
更新渲染階段構(gòu)建 hook 鏈表的算法就比較麻煩。我們從 fiber 開始
我們知道 React 在 render 階段會(huì)復(fù)用 fiber 節(jié)點(diǎn),假設(shè)我們第一次渲染完成的 fiber 節(jié)點(diǎn)如下:
var firstFiber = { ..., // 省略其他屬性 alternate: null, // 由于是第一次渲染,alternate為null memoizedState, // 第一次渲染構(gòu)建的hook鏈表 updateQueue, // 第一次渲染收集的effect list };
經(jīng)過第一次渲染以后,我們將得到下面的 hook 鏈表:
當(dāng)我們點(diǎn)擊按鈕觸發(fā)更新,renderWithHooks 函數(shù)開始調(diào)用,但 Home 函數(shù)執(zhí)行前,此時(shí)workInProgressHook
、currentHook
都為 null。同時(shí)新的 fiber 的memoizedState
、updateQueue
都被重置為 null
workInProgressHook
用于構(gòu)建新的 hook 鏈表
currentHook
用于遍歷上一次渲染構(gòu)建的 hook 鏈表,即舊的鏈表,或者當(dāng)前的鏈表(即和當(dāng)前顯示的頁(yè)面對(duì)應(yīng)的 hook 鏈表)
按照本例中調(diào)用 hook 函數(shù)的順序,一步步拆解updateWorkInProgressHook
算法的過程
- 第一步 調(diào)用 useState
由于此時(shí) currentHook
為 null,因此我們需要初始化它指向舊的 hook 鏈表的第一個(gè) hook 對(duì)象。
if (currentHook === null) { var current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } currentHook = nextCurrentHook;
創(chuàng)建一個(gè)新的 hook 對(duì)象,復(fù)用上一次的 hook 對(duì)象的狀態(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
此時(shí) currentHook 已經(jīng)有值,指向第一個(gè) hook 對(duì)象。因此將 currentHook 指向它的下一個(gè) hook 對(duì)象,即第二個(gè)
if (currentHook === null) { } else { nextCurrentHook = currentHook.next; } currentHook = nextCurrentHook;
同樣的,也需要為 useRef 創(chuàng)建一個(gè)新的 hook 對(duì)象,并復(fù)用上一次的 hook 狀態(tài)
后面的 hook 的執(zhí)行過程和 useRef 一樣,都是一邊遍歷舊的 hook 鏈表,為當(dāng)前 hook 函數(shù)創(chuàng)建新的 hook 對(duì)象,然后復(fù)用舊的 hook 對(duì)象的狀態(tài)信息,然后添加到 hook 鏈表中
從更新渲染的過程也可以看出,hook 函數(shù)的執(zhí)行是會(huì)遍歷舊的 hook 鏈表并復(fù)用舊的 hook 對(duì)象的狀態(tài)信息。這也是為什么我們不能將 hook 函數(shù)寫在條件語(yǔ)句或者循環(huán)中的根本原因,我們必須保證 hook 函數(shù)的順序在任何時(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react 原生實(shí)現(xiàn)頭像滾動(dòng)播放的示例
這篇文章主要介紹了react 原生實(shí)現(xiàn)頭像滾動(dòng)播放的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04ReactNative頁(yè)面跳轉(zhuǎn)Navigator實(shí)現(xiàn)的示例代碼
本篇文章主要介紹了ReactNative頁(yè)面跳轉(zhuǎn)Navigator實(shí)現(xiàn)的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08React?Hook?Form?優(yōu)雅處理表單使用指南
這篇文章主要為大家介紹了React?Hook?Form?優(yōu)雅處理表單使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03react系列從零開始_簡(jiǎn)單談?wù)剅eact
下面小編就為大家?guī)硪黄猺eact系列從零開始_簡(jiǎn)單談?wù)剅eact。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07一文詳解手動(dòng)實(shí)現(xiàn)Recoil狀態(tài)管理基本原理
這篇文章主要為大家介紹了一文詳解手動(dòng)實(shí)現(xiàn)Recoil狀態(tài)管理基本原理實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05