React 原理系列 —— Hook 是這樣工作的
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
Part 1 函式式元件和 Hook
通常情況下,我們在函式式元件中這樣呼叫 hook:
function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>add</button> </div> ); }
函式式元件本身是個純函式,沒有任何狀態,它通過呼叫 useState 獲取一個狀態和改變狀態的方法。但這個 useState 是 React 匯出的“全域性”函式,和當前函式式元件沒有任何顯式關聯。所以必定有某種力量將一個函式式元件例項和它用到的 state 繫結起來。
Fiber 上的 Hook
這個力量就是 Fiber,在 Fiber 節點中有兩個相關屬性:
- type:指向 Component,可能是 Function Component,也可能是 Class Component 等其他元件。對 Function Component 來說,就是一個具體的 render 函式,比如上面的 Example 函式。
- memoizedState:指向自身狀態,在 Class Fiber 下是建構函式宣告的 state,在 Function Fiber 下則是一個 Hook 池。Hook 池中維護著元件呼叫 useXXX 產生的所有 Hook,Hook 中又分別記著各自的狀態。這樣就實現了 Hook 和 Fiber 的繫結。
Hook 池和 Fiber 繫結的意義在於,當某個 Function Component 的 Fiber 開始 render,它能根據狀態池定位到上一次 render 的 Hook 狀態,為本次 render 執行的所有 useXXX 提供行為依據,“一次性”函式就有了“延續”的狀態。
找到“上一次”
任意一個 Function Component Fiber 更新時都會走到 renderWithHooks 方法:
function updateFunctionComponent( current, workInProgress, Component, nextProps, renderExpirationTime ) { nextChildren = renderWithHooks(current,workInProgress,Component, nextProps,context,renderExpirationTime); }
這個方法在 ReactFiberHooks 模組中,模組裡有全域性的 nextCurrentHook 指標,表明當前指向的 Hook。renderWithHooks 會首先切換 nextCurrentHook 到當前 Fiber 的 Hook 池,再執行 render 函式,然後 render 函式中呼叫的所有“全域性”useXXX 都從這個指標獲取“上一次”。
function renderWithHooks(current,workInProgress,Component,props,refOrContext,nextRenderExpirationTime) { // 切換 nextCurrentHook nextCurrentHook = current !== null ? current.memoizedState : null; // 執行 render 方法 let children = Component(props, refOrContext); return children; }
弄清楚 Hook 和 Fiber 的繫結和切換,接下來我們進到一個 Fiber 節點內部,看看 Hook 池的維護機制。
Part 2 Hook 池的維護機制
Hook 機制的所有實現,都在前面提到的 ReactFiberHooks 模組中。
Hook 的資料結構
Hook 是一個物件,render 中呼叫 useXXX 方法,就會建立一個 Hook 物件。
type Hook = { memoizedState: any; baseState: any; baseUpdate: Update<any, any> | null; queue: UpdateQueue<any, any> | null; next: Hook | null; }
如果你呼叫的方法不一樣,Hook 物件裡面的欄位搭載的資訊也不一樣。比如 useState、useReducer 這樣的 State Hook,和 useEffect 這樣的 Effect Hook,就會在 memoizedState 上存不同的東西。
當元件呼叫多次 useXXX,就會建立多個 Hook。同一 Component 的多個 Hook 之間用連結串列連線起來,構成 Hook 池,Fiber 的 memoizedState 就指向池中第一個 Hook:
除了 nextCurrentHook,ReactFiberHooks 提供了一些其他指標來做遍歷:
let currentHook: Hook | null = null; let nextCurrentHook: Hook | null = null; let firstWorkInProgressHook: Hook | null = null; let workInProgressHook: Hook | null = null; let nextWorkInProgressHook: Hook | null = null;
前兩個指標用於遍歷已有的 Hook 池,後三個指標用來構建一個新 Hook 池。
方法換檔
雖然看起來 Function Component 每次 render 都呼叫的同一個 useXXX,但實際上 mount 和 update 呼叫對 Hook 池是幾乎完全不同的操作。
因此 ReactFiberHooks 提供了一個換檔機制:宣告兩套 HooksDispatcher,上面綁定了 mount、update 階段不同的 Hook 實現。當一個 Function Component 準備 render 時,判斷它是 mount 還是 update,切換不同的 HooksDispatcher。
具體的換擋邏輯仍在 renderWithHooks 中,而對 mount 還是 update 的判斷,則依賴當前 Fiber 是否有 Hook 池:
nextCurrentHook = current !== null ? current.memoizedState : null; ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
這裡其實不合理,如果元件沒用到 Hook,即使在 update 階段,也不會有 Hook 池。但也無所謂,反正發動機沒轉,換哪個檔都不會動。既然能換擋,我們也就能看到不同階段下,Hook 池的構建和讀取方式。
Hook 池的構建
Fiber mount 階段,Fiber 上沒有 Hook 池,從頭構建:
- 構造新 Hook。
- 接入連結串列。如果 Fiber 上沒有 Hook,說明當前 Hook 是整個函式式元件的第一個 Hook,放 firstWorkInProgressHook 最後接造 Fiber 上;如果有 Hook,直接 next 接下去。
- 更新指標。
這部分邏輯抽象在每個 mountXXX 都要調的 mountWorkInProgressHook 中。
function mountWorkInProgressHook(): Hook { // 1. 構造新 Hook const hook: Hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; // 2. 接入連結串列;3. 更新指標 if (workInProgressHook === null) { firstWorkInProgressHook = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
mountWorkInProgressHook 返回建立的 Hook,隨後不同的 mountXXX 會再對 Hook 做各自的處理,後面分開說。
等整個 Hook 池構建完畢,在 renderWithHooks 中掛到 Fiber 的 memoizedState 上:
// renderWithHooks 方法 renderedWork.memoizedState = firstWorkInProgressHook;
Hook 池的更新
Fiber 開始 update,通過 renderWithHooks 讀取 Fiber.memoizedState 上的第一個 Hook,並給到 nextCurrentHook 指標。
nextCurrentHook = current !== null ? current.memoizedState : null;
然後每個逐個進入 updateXXX。我們以第一個 Hook 操作為例,會調通用的 updateWorkInProgressHook 方法執行以下操作:
- 克隆當前 Hook。為什麼不直接複用?這樣可以保證 Fiber 上的 Hook “原件”完整,某些情況下(比如 useEffect 要對比新舊 Hook 的依賴),在構建當前 Hook 的同時,仍需要上一次 Hook 的資訊。
- 更新指標。沿著 next 把“當前 Hook”指標指向連結串列的下一次節點,同時 workInProgressHook 也通過 next 構建下去。這就是網紅問題 “為什麼 Hook 不能用在 if / for 語句裡?” 的答案,一旦中間某次 useXXX 沒 next,會導致後續所有 Hook 取錯。
附部分程式碼:
function updateWorkInProgressHook() { currentHook = nextCurrentHook; // 1. 克隆 Hook const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, next: null, }; // 2. 更新指標 if (workInProgressHook === null) { workInProgressHook = firstWorkInProgressHook = newHook; } else { workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; return workInProgressHook; }
等 Hook 池更新完畢,renderWithHooks 同樣會把 Fiber 的 memoizedState 切換到 firstWorkInProgressHook。
小結
這節介紹了 Hook 的基本機制:
- 函式內的 Hook 呼叫建立一個 Hook 物件,上面儲存著狀態資料。
- Hook 維護在一個 Hook 池中,並掛到 Fiber 節點上,Hook 池是一個單向連結串列。
- Fiber 在 mount 和 update 階段通過“換擋”切換 dispatcher,呼叫不同的 useXXX 實現。
- mount 階段要構建 Hook 池;update 階段則逐個克隆 Hook,構建新的 Hook 池。
Part 3 State Hook:提供狀態
State Hook 來自 useState、useReducer,用來給函式式元件提供狀態,它的實現圍繞狀態及其更新。
const [ state, dispatch ] = useState(initial);
State Hook 結構
在 State Hook 中,memoizedState 儲存著 Hook 的最新狀態,baseState 則是初始狀態(useState 傳入)。queue 儲存著這個 hook 的更新佇列,資料結構為單向迴圈連結串列:
State Hook 構建:mount 階段
State Hook 經過通用的構造階段,執行自己的邏輯,我們補充到下圖中:
- 狀態初始賦值。也就是我們呼叫 useState 的入參。
- 構造更新佇列,繫結 dispatch。更新佇列會在需要更新的時候被“清洗”來計算最新 memoizedState,而 dispatch 是修改佇列的唯一入口。
- 返回 state 和 dispatch。這樣我們第一次呼叫 useState 完成,獲得 Hook 的初始狀態值和更新狀態的方法。
附程式碼:
function mountState() { const hook = mountWorkInProgressHook(); // 4. 狀態初始賦值 hook.memoizedState = hook.baseState = initialState; // 5. 構造更新佇列 const queue = (hook.queue = { last, dispatch, lastRenderedReducer, lastRenderedState }); const dispatch = (queue.dispatch = (dispatchAction.bind( null, ((currentlyRenderingFiber: any): Fiber), queue, ))); // 6. 返回 state 和 dispatch return [hook.memoizedState, dispatch]; }
dispatchAction 在 mount 階段繫結到 Hook 上並返回,後續的更新直接來 Hook 上調就好。
State Hook 讀取:update 階段
Fiber update 階段,useState 從 Hook 池中克隆出 Hook,獲取歷史狀態,計算新狀態。以第一個 Hook 操作為例,補齊後續幾步:
- 計算最新 state。這是個清理 Update Queue 的動作,可選,如果兩次 render 之間沒有對這個 Hook 節點的 set 操作,就不會有 Update,可以直接返回現有 state。
- 返回最新 state 和 dispatch。
附部分程式碼:
function updateReducer() { const hook = updateWorkInProgressHook(); const queue = hook.queue; // 3. 合併 queue,計算最新 memoizedState let newState = hook.memoizedState; let update = firstRenderPhaseUpdate; do { const action = update.action; newState = reducer(newState, action); update = update.next; } while (update !== null); hook.memoizedState = newState; const dispatch: Dispatch<A> = (queue.dispatch: any); // 4. return [hook.memoizedState, dispatch]; }
State Hook 更新:dispatchAction
dispatchAction 是第一次調 useState 時返回出去的,作為 setXXX 呼叫。那很顯然,setXXX 把傳入的更新加入 Hook 更新佇列,並觸發一次更新。這裡借用之前那篇的圖:( React Fiber 架構 —— “更新”到底是個啥 - 知乎 )
很簡單的插入連結串列操作,完成後通過 scheduleWork 發起更新。
程式碼:
function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) { // 1. 構造更新 const update: Update<S, A> = { expirationTime, action, eagerReducer: null, eagerState: null, next: null, }; // 2. 插入佇列 const last = queue.last; if (last === null) { update.next = update; } else { const first = last.next; if (first !== null) { update.next = first; } last.next = update; } queue.last = update; // 3. 發起排程 scheduleWork(fiber, expirationTime); }
小結
這節介紹了 Stete Hook 的實現:
- State Hook 來自 useState、useReducer,用來提供狀態及其更新。
- State Hook 通過 memoizedState 儲存狀態,通過 queue 維護更新佇列的資料和方法(dispatch)。
- State Hook 的更新佇列是個單向迴圈連結串列。
- 更新階段的 State Hook 會“清洗”更新佇列,計算並返回最新 memoizedState。
- 我們呼叫 useState 返回的 dispatch,就是建立並在更新佇列中插入新更新,併發起整體排程。
Part 4 Effect Hook:依賴監聽和清理
Effect Hook 來自 useEffect、useLayoutEffect,為函式式元件提供依賴監聽。
useEffect(() => { // create Effect return () => { // destroy Effect }; }, [deps]);
Effect Hook 結構
在 Effect Hook 上,只有 memoizedState 被用到,用來儲存一個 Effect 物件。這個物件會被插入 Fiber 的更新佇列,告訴 Fiber 更新後要做哪些動作。
type Effect = { tag: HookEffectTag, create: () => (() => void) | void, destroy: (() => void) | void, deps: Array<mixed> | null, next: Effect, };
- create:就是我們傳入的第一個回撥函式,在依賴變化的時候執行
- destroy:用來記錄 create 執行後的返回函式。某些階段來自於上一次同一個 Effect Hook 的 create 執行結果,某些階段來自於自身。
- deps:就是我們傳入的依賴陣列,用來進行依賴對比。
- tag:決定 Effect 在 Fiber 提交時如何被處理。這個很關鍵,它的值來自於 useEffect 的執行時機和依賴變化情況。
Effect Hook 構建和更新
第一次渲染,走到 mountEffect,通過通用 mountWorkInProgressHook 構造並插入一個 Hook 返回。然後對這個 Hook 做一些操作,我們關注 4、5、6 步:
- 構造 Effect(副作用物件)。記錄了副作用的依賴陣列、回撥函式、清理函式、處理方式。
- Effect 加入 Fiber 節點的 updateQueue。updateQueue 是個連結串列,連結串列元素就是我們構造的 Effect,這個連結串列會在 Fiber 更新完畢後逐個根據 Effect 物件提供的資訊處理和清空。
- Effect 同時也會掛到當前 Hook 上。
構建程式碼:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); } function pushEffect(tag, create, destroy, deps) { const effect: Effect = {...}; const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } return effect; }
更新執行函式式元件時,每個 useEffect 又會被分別執行一次,同樣要分別克隆 Hook,然後構造 Effect 掛到 updateQueue 和 Hook 上。
- 對比舊 Hook,確定 Effect 物件的屬性。這是因為 Effect 的內容要依賴前面的 Effect,比如銷燬函式(destroy)就是由上一次執行建立函式(create)返回的。下一節我們展開看。
- 構造 Effect
- Effect 入隊
- Effect 掛到當前 Hook 上
程式碼:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { // 4. 對比舊 Hook,確定 Effect 物件的屬性 // ...暫時省略,後面展開說 } hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); }
至此,無論 Fiber 初始化還是更新,我們所有 useEffect 產生的 Effect,都可以從 Hook 池和 Update Queue 訪問到。其中 Hook 池方便我們管理所有 Hook(包括之前 State Hook)的資料,Effect 就是 Effect Hook 的資料;Update Queue 則為了方便 Fiber 直接拿到 Effect 執行副作用。
Effect 屬性的取值和流轉
接下來我們看看 Effect 在構造過程中,在不同場景下,是如何根據使用者傳參、Effect 之間關係,來確定自身屬性的。
Effect 由 pushEffect 函式建立,傳入的四個引數直接賦值給對應屬性:
function pushEffect(tag, create, destroy, deps) { const effect: Effect = { tag, create, destroy, deps, }; // ... }
pushEffect 的呼叫入參,直接決定了 Effect 的內容。
初始化建立 Effect
在 mountEffectImpl 中,是對 Effect 的初始化建立: pushEffect(hookEffectTag, create, undefined, nextDeps)
,對應屬性傳值如下:
- tag:也就是 hookEffectTag,值為 mountEffect 傳入的
UnmountPassive | MountPassive
=>0b10000000 | 0b01000000
=>0b11000000
- create:useEffect 的第一個入參函式
- destroy:傳入一個 undefined,因為初始化建立的 Effect 不存在“前一次執行”
- deps:依賴陣列
Effect 被執行
然後初始化建立的 Effect 會在 Fiber commit 的時候被執行,在 commitWork 階段一個叫 commitHookEffectList 的方法中,對 Effect 做了這樣的動作:
const create = effect.create; effect.destroy = create();
這時 Effect 的 destroy 被「暫時」掛上了自己 create 的返回。這就是前面說的「某些階段來自於上一次同一個 Effect Hook 的 create 執行結果,某些階段來自於自身。」
但這樣不會亂嗎?上一次 destroy 不就找不到了嗎?不會。因為 Effect 總是先執行上一次 destroy,再執行自己的 create,此時上一次 destroy 已經執行過沒用了,正好空出來 destroy 屬性掛自己的,方便傳給下一次 Effect。
更新建立 Effect
Fiber 更新階段再執行 useEffect,來到 updateEffectImpl。這裡首先會去拿上一次構造的 Effect,再看這張圖:
新 Hook 被克隆出來,上一次 Hook 則留在 currentHook 指標上,通過 currentHook.memoizedState
拿到它的 Effect,以及Effect 的依賴(deps)和銷燬函式(destroy,按前面所說,此時得到的 destroy 就是上一次 Effect 自己的)。
接著會做一件重要的事: 依賴對比 ,依賴是否變化,對當前 Effect 的處理方式影響很大。
依賴變化是怎麼對比出來的?遍歷 + 淺比較。
// areHookInputsEqual 方法 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; // is 方法 return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
當依賴有變化,建立的 Effect 屬性值如下:
- tag:也就是 hookEffectTag,值為 updateEffect 傳入的
UnmountPassive | MountPassive
=>0b10000000 | 0b01000000
=>0b11000000
- create:useEffect 的第一個入參函式
- destroy:上一次 Effect 的 destroy
- deps:依賴陣列
當依賴沒變化,Effect tag 會變成 NoHookEffect
=> 0b00000000
。
小結
至此我們摸清了在被 Fiber commit 處理前,Update Queue 中的 Effect 屬性來源:
- tag:需要處理的時候(mount 或依賴有變的 update)為
0b11000000
,不需要處理的時候(依賴不變的 update)為0b00000000
- create:useEffect 的第一個入參函式
- destroy:上一次 Effect 的 destroy
- deps:依賴陣列
處理 Fiber 上的 Effect
useEffect 產生的所有 Effect 都加入 Fiber 的 Update Queue,由 Fiber 在 commit 階段統一處理。
在 commitRoot 入口,對 FunctionComponent 這樣呼叫 commitHookEffectList 方法:
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
其中前兩個引數是常量 tag,用來和 Effect tag 比較判斷 Effect 的處理方式,對應方法引數 unmountTag、mountTag;第三個引數 finishedWork 傳入當前 Fiber。commitHookEffectList 被先後調了兩次,從傳參來看,NoHookEffect 是 0b00000000 置空,兩次的 UnmountPassive(0b10000000) 和 MountPassive(0b01000000) 分別啟用 unmountTag、mountTag,依次執行 umount 和 mount 操作。
commitHookEffectList 是統一處理 Update Queue 中 Effect 的入口,再回顧下 Update Queue 的結構:
佇列裡第一個 Effect 可以通過 const firstEffect = finishedWork.updateQueue.lastEffect.next
拿到,然後按照單向迴圈連結串列遍歷:
// 遍歷 Update Queue do { // 處理 Effect effect = effect.next; } while (effect !== firstEffect);
Unmount 處理
處理 Effect 的方式判斷很巧妙,通過一個二進位制位運算: (effect.tag & unmountTag) !== NoHookEffect
,判斷是否需要進行 umount 處理。NoHookEffect 是個 0,那就要 effect.tag 和 unmountTag 不同且都不為 0,條件判斷才為 true。結合 Effect 的屬性值和 commitHookEffectList 傳參內容,我們列舉出以下幾種情況:
- Effect 依賴沒變(effect.tag = 0),則肯定不做 unmount 處理
- 第二次 commitHookEffectList(unmountTag = 0),也肯定不做 ummount 處理
- Effect 依賴有變(effect.tag = 0b11000000),且第一次 commitHookEffectList(unmountTag = 0b10000000),則做 unmount 處理:執行 effect.destroy(上一次 Effect 的銷燬函式)
if ((effect.tag & unmountTag) !== NoHookEffect) { const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } }
Mount處理
也通過二進位制來做: (effect.tag & mountTag) !== NoHookEffect
:
- Effect 依賴沒變(effect.tag = 0),則肯定不做 mount 處理
- 第一次 commitHookEffectList(mountTag = 0),也肯定不做 mount 處理
- Effect 依賴有變(effect.tag = 0b11000000),且第二次 commitHookEffectList(mountTag = 0b01000000),則做 mount 處理:執行 effect.create(回撥 useEffect 入參,並把返回作為 destroy 掛到 Effect 上)
if ((effect.tag & mountTag) !== NoHookEffect) { const create = effect.create; effect.destroy = create(); }
小結
這節介紹了 Effect Hook 實現:
- Effect Hook 來自 useEffect、useLayoutEffect。
- Effect Hook 通過 memoizedState 儲存一個 useEffect 產生的 Effect 物件。
- Effect 物件儲存著建立(create)回撥、銷燬(destroy)回撥、依賴、處理標記。
- Effect 會同時掛到 Fiber 的 Update Queue 上,方便 Fiber 在 commit 階段找到並執行,Update Queue 是個單向迴圈連結串列。
- 更新階段,Effect 會找到同一呼叫在上一次構建的 Effect,對比依賴以決定被如何處理,並獲取 destroy。
Part 5 其他 Hook
經過前面對 Hook 池和兩種關鍵 Hook 的介紹,我們基本摸清了 Hook 的套路:呼叫 useXXX —> 構建 Hook 維護 Hook 池 —> 在 Hook 上加一些不同 useXXX 特有的資料和邏輯。以此類推,就很容易猜到其他 Hook 的實現方式了。
useMemo
useMemo 返回一個memoized 值。把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Memo Hook 上要存什麼狀態?依賴值和觸發的計算值,實現上是一個數組。然後把計算值返回出去:
// mountMemo 和 updateMemo 方法 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue;
但在 update 階段,上述邏輯是有條件的,即“依賴有變化”,否則只要返回上一次計算值就好。所以 useEffect 曾用到的 areHookInputsEqual 又出場了:
// updateMemo 方法 const prevState = hook.memoizedState; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue;
useCallback
同 useMemo 十分相似,不同點是入參的方法不執行,變為直接儲存:
// mountMemo 和 updateMemo 方法 hook.memoizedState = [callback, nextDeps]; return nextValue;
useRef
useRef 就像是可以在其 .current 屬性中儲存一個可變值的“盒子”,而這個“盒子”的引用值始終不變。
只要在 mount 時建立一個物件,存到 Hook 上,後續 update 直接取:
// mountRef const ref = {current: initialValue}; hook.memoizedState = ref; return ref; // updateRef return hook.memoizedState;
Part Z 總結
本篇介紹了 Hook 的實現原理:
- Function Component 中每次呼叫 useXXX,都會建立一個 Hook,這些 Hook 以 Hook 池的形式維護在 Fiber.memoizedState 上。
- Hook 是一個物件,上面存著對應呼叫的資料。不同 useXXX 方法在 Hook 上存的東西也不同。
- Function 初次執行和後續更新執行,Hook 池的維護方式、useXXX 要做的事大不相同,所以 React 會判斷當前 Fiber 所處階段(mount 或 update),換擋呼叫不同的 useXXX 實現。
- Fiber mount 時,Hook 被依次從頭建立;update 時則逐個從舊 Hook 池中克隆,構造成新 Hook 池後再切換 Fiber.memoizedState,這樣能保留舊 Hook,提供“新舊 Hook 對比”的能力。
- State Hook(useState、useReducer)在 Hook 物件上儲存狀態值和更新佇列,update 階段會清理更新佇列並計算最新狀態值。useState、useReducer 返回的 dispatch 方法用來把更新加入佇列,併發起一次更新排程。
- Effect Hook(useEffect)在 Hook 物件上儲存 Effect,一個記錄副作用建立、銷燬、依賴的物件。update 階段構建新的 Effect,並對比新老 Hook 上 Effect 的依賴,決定 Effect 處理方式。Effect 會被同時記錄在 Fiber 自身的更新佇列裡,等待 Fiber commit 後統一處理。
- React 原理系列 —— Hook 是這樣工作的
- A100 買不到了,只有小顯示卡怎麼訓大模型
- MedISeg:面向醫學影象語義分割的技巧、挑戰和未來的方向
- CoRL 2022 | SurroundDepth: 自監督環視深度估計
- 【機器學習】邏輯迴歸(非常詳細)
- dnn實踐-特徵處理
- Google資料安全自動化建設之路(白皮書)
- 用typescript型別來實現快排
- 基於自建 VTree 的全鏈路埋點方案
- 除了鮑威爾講話,全球央行年會還揭露了什麼?
- coost v3.0.0 (微型boost庫)釋出
- 仔細研究 Go(golang) 型別系統
- 乾貨 | 嵌入式資料分析最佳實踐
- 關於高頻量化交易的程式碼專案
- 徹底解決 qiankun 找不到入口的問題
- VIM 外掛推薦
- 全網最通透:MySQL 的 redo log 保證資料不丟的原理
- 蟻群演算法的簡要分析
- 7月美聯儲會議紀要
- 容器平臺架構之道