- React 源碼版本: v16.11.0
- 源碼註釋筆記:airingursb/react
在寫本文之前,事先閱讀了網上了一些文章,關於 Hooks 的源碼解析要麼過於淺顯、要麼就不細致,所以本文著重講解源碼,由淺入深,爭取一行代碼也不放過。那本系列講解第一個 Hooks 便是 useState,我們將從 useState 的用法開始,再闡述規則、講解原理,再簡單實現,最後源碼解析。另外,在本篇開頭,再補充一個 Hooks 的概述,前兩篇・限於篇幅問題一直沒有寫一塊。
注:距離上篇文章已經過去了兩個月,這兩個月業務繁忙所以沒有什麼時間更新該系列的文章,但 react 這兩個月卻從 16.9 更新到了 16.11,review 一下這幾次的更新都未涉及到 hooks,所以我也直接把源碼筆記這塊更新到了 16.11。
1. React Hooks 概述#
Hook 是 React 16.8 的新增特性,它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。其本質上就是一類特殊的函數,它們約定以 use
開頭,可以為 Function Component 注入一些功能,賦予 Function Component 一些 Class Component 所具備的能力。
例如,原本我們說 Function Component 無法保存狀態,所以我們經常說 Stateless Function Component,但是現在我們借助 useState 這個 hook 就可以讓 Function Component 像 Class Component 一樣具有狀態。前段時間 @types/react 也將 SFC 改成了 FC。
1.1 動機#
在 React 官網的 Hook 簡介中列舉了推出 Hook 的原因:
- 在組件之間復用狀態邏輯很難
- 複雜組件變得難以理解
- 難以理解的 class
一,組件之間復用狀態邏輯很難。是我們系列第二篇中一直討論的問題,此處不再贅述。
二,複雜組件變得難以理解,即組件邏輯複雜。主要是針對 Class Component 來說,我們經常要在組件的各種生命週期中編寫代碼,如在 componentDidMount 和 componentDidUpdate 中獲取數據,但是在 componentDidMount 中可能也包括很多其他的邏輯,使得組件越開發越臃腫,且邏輯明顯扎堆在各種生命週期函數中,使得 React 開發成為了 “面向生命週期編程”。而 Hooks 的出現,將這種這種 “面向生命週期編程” 變成了 “面向業務邏輯編程”,使得開發者不用再去關心本不該關心的生命週期。
三,難以理解的 class,表現為函數式編程比 OOP 更加簡單。那么再深入一些去考慮性能,Hook 會因為在渲染時創建函數而變慢嗎?答案是不會,在現在瀏覽器中閉包和類的原始性能只有在極端場景下又有明顯的區別。反而,我們可以認為 Hook 的設計在某些方面會更加高效:
- Hook 避免了 class 需要的額外開支,像是創建類實例和在構造函數中綁定事件處理器的成本。
- 符合語言習慣的代碼在使用 Hook 時不需要很深的組件樹嵌套。這個現象在使用高階組件、render props、和 context 的代碼庫中非常普遍。組件樹小了,React 的工作量也隨之減少。
其實,React Hooks 帶來的好處不僅是更函數式、更新粒度更細、代碼更清晰,還有以下三個優點:
- 多個狀態不會產生嵌套,寫法還是平鋪的:如 async/await 之於 callback hell 一樣,hooks 也解決了高階組件的嵌套地獄問題。雖然 renderProps 也可以通過 compose 解決這個問題,但使用略為繁瑣,而且因為強制封裝一個新對象而增加了實體數量。
- Hooks 可以引用其他 Hooks,自定義 Hooks 更加靈活。
- 更容易將組件的 UI 與狀態分離。
1.2 Hooks API#
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useResponder
以上 Hooks API 都會在未來一一講解,此處不再贅述。本文先講解 useState。
1.3 自定義 Hooks#
通過自定義 Hook,可以將組件邏輯提取到可重用的函數中。這裡安利一個網站:https://usehooks.com/,裡面收集了實用的自定義 Hooks,可以無縫接入項目中使用,充分體現了 Hooks 的可復用性之強、使用之簡單。
2. useState 的用法與規則#
import React, { useState } from 'react'
const App: React.FC = () => {
const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string>('airing')
const [age, setAge] = useState<number>(18)
return (
<>
<p>你點擊了 {count} 次</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
點擊我
</button>
</>
)
}
export default App
如果用過 redux 的話,這一幕一定非常眼熟。給定一個初始 state,然後通過 dispatch 一個 action,再經由 reducer 改變 state,再返回新的 state,觸發組件重新渲染。
它等價於下面這個 Class Component:
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
age: 18,
name: 'airing'
};
}
render() {
return (
<>
<p>你點擊了 {this.state.count} 次</p>
<button onClick={() => this.setState({
count: this.state.count + 1,
age: this.state.age + 1
})}>
點擊我
</button>
</>
);
}
}
export default App
可以看到 Function Component 比 Class Component 簡潔,useState 的使用也非常簡單。但需要注意的是,Hooks 的使用必須要符合這條規則:確保 Hook 在每一次渲染中都按照同樣的順序被調用。因此最好每次只在最頂層使用 Hook,不要在循環、條件、嵌套函數中調用 Hooks,否則容易出錯。
那麼,為什麼我們必須要滿足這條規則?接下來,我們看一下 useState 的實現原理並自己親自動手實現一個 useState 便可一目了然。
3. useState 的原理與簡單實現#
3.1 Demo 1: dispatch#
第二節中我們發現 useState 的用法蠻像 Redux 的,那我們基於 Redux 的思想,自己動手實現一個 useState:
function useState(initialValue) {
let state = initialValue
function dispatch(newState) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
我們將從 React 中引入的 useState 替換成自己實現的:
import React from 'react'
import { render } from 'react-dom'
function useState(initialValue: any) {
let state = initialValue
function dispatch(newState: any) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
const App: React.FC = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('airing')
const [age, setAge] = useState(18)
return (
<>
<p>你點擊了 {count} 次</p>
<p>你的年齡是 {age}</p>
<p>你的名字是 {name}</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
點擊我
</button>
</>
)
}
export default App
這個時候我們發現點擊按鈕不會有任何響應,count 和 age 都沒有變化。因為我們實現的 useState 並不具備存儲功能,每次重新渲染上一次的 state 就重置了。這裡想到可以在外部用個變量來存儲。
3.2 Demo 2: 記憶 state#
基於此,我們優化一下剛才實現的 useState:
let _state: any
function useState(initialValue: any) {
_state = _state || initialValue
function setState(newState: any) {
_state = newState
render(<App />, document.getElementById('root'))
}
return [_state, setState]
}
雖然按鈕點擊有變化了,但是效果不太對。如果我們刪掉 age 和 name 這兩個 useState 會發現效果是正常的。這是因為我們只用了單個變量去儲存,那自然只能存儲一個 useState 的值。那我們想到可以用備忘錄,即一個數組,去儲存所有的 state,但同時我們需要維護好數組的索引。
3.3 Demo 3: 備忘錄#
基於此,我們再次優化一下剛才實現的 useState:
let memoizedState: any[] = [] // hooks 的值存放在這個數組裡
let cursor = 0 // 當前 memoizedState 的索引
function useState(initialValue: any) {
memoizedState[cursor] = memoizedState[cursor] || initialValue
const currentCursor = cursor
function setState(newState: any) {
memoizedState[currentCursor] = newState
cursor = 0
render(<App />, document.getElementById('root'))
}
return [memoizedState[cursor++], setState] // 返回當前 state,並把 cursor 加 1
}
我們點擊三次按鈕之後,打印出 memoizedState 的數據如下:
打開頁面初次渲染,每次 useState 執行時都會將對應的 setState 綁定到對應索引的位置,然後將初始 state 存入 memoizedState 中。
在點擊按鈕的時候,會觸發 setCount 和 setAge,每個 setState 都有其對應索引的引用,因此觸發對應的 setState 會改變對應位置的 state 的值。
這裡是模擬實現 useState,所以每次調用 setState 都有一次重新渲染的過程。
重新渲染依舊是依次執行 useState,但是 memoizedState 中已經有了上一次是 state 值,因此初始化的值並不是傳入的初始值而是上一次的值。
因此剛才在第二節中遺留問題的答案就很明顯了,為什麼 Hooks 需要確保 Hook 在每一次渲染中都按照同樣的順序被調用?因為 memoizedState 是按 Hooks 定義的順序來放置數據的,如果 Hooks 的順序變化,memoizedState 並不會感知到。因此最好每次只在最頂層使用 Hook,不要在循環、條件、嵌套函數中調用 Hooks。
最後,我們來看看 React 中是怎樣實現 useState 的。
4. useState 源碼解析#
4.1 入口#
首先在入口文件 packages/react/src/React.js 中我們找到 useState,其源自 packages/react/src/ReactHooks.js。
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
resolveDispatcher () 返回的是 ReactCurrentDispatcher.current,所以 useState 其實就是 ReactCurrentDispatcher.current.useState。
那麼,ReactCurrentDispatcher 是什麼?
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
}
我們最終找到了 packages/react-reconciler/src/ReactFiberHooks.js,在這裡有 useState 具體實現。該文件也包含了所有 React Hooks 的核心處理邏輯。
4.2 類型定義#
4.2.1 Hook#
在開始之前,我們先看看 ReactFiberHooks.js 中幾個類型的定義。首先是 Hooks:
export type Hook = {
memoizedState: any, // 指向當前渲染節點 Fiber, 上一次完整更新之後的最終狀態值
baseState: any, // 初始化 initialState, 已經每次 dispatch 之後 newState
baseUpdate: Update<any, any> | null, // 當前需要更新的 Update ,每次更新完之後,會賦值上一个 update,方便 react 在渲染錯誤的邊緣,數據回溯
queue: UpdateQueue<any, any> | null, // 緩存的更新隊列,存儲多次更新行為
next: Hook | null, // link 到下一個 hooks,通過 next 串聯每一 hooks
};
可以看到,Hooks 的數據結構和我們之前自己實現的基本一致,memoizedState 也是一個數組,準確來說 React 的 Hooks 是一個單向鏈表,Hook.next 指向下一個 Hook。
4.2.2 Update & UpdateQueue#
那麼 baseUpdate 和 queue 又是什麼呢?先看一下 Update 和 UpdateQueue 的類型定義:
type Update<S, A> = {
expirationTime: ExpirationTime, // 當前更新的過期時間
suspenseConfig: null | SuspenseConfig,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A> | null, // link 下一個 Update
priority?: ReactPriorityLevel, // 優先級
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
Update 稱作一個更新,在調度一次 React 更新時會用到。UpdateQueue 是 Update 的隊列,同時還帶有更新時的 dispatch。具體的 React Fiber 和 React 更新調度的流程本篇不會涉及,後續會有單獨的文章補充講解。
4.2.3 HooksDispatcherOnMount & HooksDispatcherOnUpdate#
還有兩個 Dispatch 的類型定義需要關注一下,一個是首次加載時的 HooksDispatcherOnMount,另一個是更新時的 HooksDispatcherOnUpdate。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createResponderListener,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createResponderListener,
};
4.3 首次渲染#
4.3.1 renderWithHooks#
React Fiber 會從 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork () 開始執行(React Fiber 的具體流程後續單獨成文補充講解),對於 Function Component,其走以下邏輯加載或更新組件:
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
在 updateFunctionComponent 中,對於 Hooks 的處理是:
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
因此,我們發現 React Hooks 的渲染核心入口是 renderWithHooks。其他的渲染流程我們並不關心,本文我們著重來看看 renderWithHooks 及其之後的邏輯。
我們回到 ReactFiberHooks.js 來看看 renderWithHooks 具體做了什麼,去除容錯代碼和 __DEV__
的部分,renderWithHooks 代碼如下:
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
nextCurrentHook = current !== null ? current.memoizedState : null;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
// sideEffectTag = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because nextCurrentHook === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so nextCurrentHook would be null during updates and mounts.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, refOrContext);
if (didScheduleRenderPhaseUpdate) {
do {
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;
// Start over from the beginning of the list
nextCurrentHook = current !== null ? current.memoizedState : null;
nextWorkInProgressHook = firstWorkInProgressHook;
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
children = Component(props, refOrContext);
} while (didScheduleRenderPhaseUpdate);
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);
renderedWork.effectTag |= sideEffectTag;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
return children;
}
renderWithHooks 包括三個部分,首先是賦值 4.1 中提到的 ReactCurrentDispatcher.current,後續是做 didScheduleRenderPhaseUpdate 以及一些初始化的工作。核心是第一部分,我們來看看:
nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
如果當前 Fiber 為空,就認為是首次加載,ReactCurrentDispatcher.current.useState 將賦值成 HooksDispatcherOnMount.useState,否則賦值 HooksDispatcherOnUpdate.useState。根據 4.2 中的類型定義,即首次加載時,useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState;更新時 useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState。
4.3.2 mountState#
首先看看 mountState 的實現:
// 第一次調用組件的 useState 時實際調用的方法
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 創建一個新的 Hook,並返回當前 workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 新建一個隊列
const queue = (hook.queue = {
last: null, // 最後一次更新邏輯, 包括 {action,next} 即狀態值和下一次 Update
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any), // 最後一次渲染組件時的狀態
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// 綁定當前 fiber 和 queue.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
4.3.3 mountWorkInProgressHook#
mountWorkInProgressHook 是創建一個新的 Hook 並返回當前 workInProgressHook,實現如下:
// 創建一個新的 hook,並返回當前 workInProgressHook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
// 只有在第一次打開頁面的時候,workInProgressHook 為空
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 已經存在 workInProgressHook 就將新創建的這個 Hook 接在 workInProgressHook 的尾部。
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
4.3.4 dispatchAction#
我們注意到 mountState 還做了一件很關鍵的事情,綁定當前 fiber 和 queue 到 dispatchAction 上:
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// 綁定當前 fiber 和 queue
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
那我們看一下 dispatchAction 是如何實現的:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 此分支為 re-render 時的 Fiber 調度處理
didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
expirationTime: renderExpirationTime,
suspenseConfig: null,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 將本次更新周期裡的更新記錄緩存進 renderPhaseUpdates 中
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 存儲所有的更新行為,以便在 re-render 流程中計算最新的狀態值
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
// ... 更新循環鏈表
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// 省略特殊情況 Fiber NoWork 時的代碼
// 創建一個更新任務,執行 fiber 的渲染
scheduleWork(fiber, expirationTime);
}
}
if 的第一個分支涉及 Fiber 的調度,我們此處僅是提及,本文不詳細講解 Fiber,只要知道 fiber === currentlyRenderingFiber
時是 re-render,即當前更新周期中又產生了新的周期即可。如果是 re-render,didScheduleRenderPhaseUpdate 置為 true,而在 renderWithHooks 中 如果 didScheduleRenderPhaseUpdate 為 true,就會循環計數 numberOfReRenders 來記錄 re-render 的次數;另外 nextWorkInProgressHook 也會有值。所以後續的代碼中,有用 numberOfReRenders > 0 來判斷是否是 re-render 的,也有用 nextWorkInProgressHook 是否為空來判斷是否是 re-render 的。
同時,如果是 re-render,會把所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map 上,以每個 Hook 的 queue 為 key。
至於最後 scheduleWork 的具體工作,我們後續單獨成文來分析。
4.4 更新#
4.4.1 updateState#
我們看看更新過程中的 useState 時實際調用的方法 updateState:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
// 第一次之後每一次執行 useState 時實際調用的方法
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
可以發現,其實 updateState 最終調用的其實是 updateReducer。對於 useState 觸發的 update action 來說,basicStateReducer 就是直接返回 action 的值(如果 action 是函數還會幫忙調用一下)。因此,useState 只是 useReduer 的一個特殊情況而已,其傳入的 reducer 為
basicStateReducer,負責改變 state,而非 useReducer 那樣可以傳入自定義的 reducer。
4.4.2 updateReducer#
那我們來看看 updateReducer 做了些什麼:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 獲取當前正在工作中的 hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
if (numberOfReRenders > 0) {
// re-render:當前更新周期中產生了新的更新
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// 所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map上,以每個 Hook 的 queue 為 key。
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
// 如果是 re-render,繼續執行這些更新直到當前渲染周期中沒有更新為止
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
if (hook.baseUpdate === queue.last) {
hook.baseState = newState;
}
queue.lastRenderedState = newState;
return [newState, dispatch];
}
}
return [hook.memoizedState, dispatch];
}
const last = queue.last;
const baseUpdate = hook.baseUpdate;
const baseState = hook.baseState;
let first;
if (baseUpdate !== null) {
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
}
} else {
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
// 循環鏈表,執行每一次更新
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateReducer 分為兩種情況:
- 非 re-render,即當前更新周期只有一個 Update。
- re-render,當前更新周期又產生了新的更新。
在 4.3.4 中我們提到 numberOfReRenders 記錄了 re-render 的次數,如果大於 0 說明當前更新周期中又產生了新的更新,那麼就繼續執行這些更新,根據 reducer 和 update.action 來創建新的 state,直到當前渲染周期中沒有更新為止,最後賦值給 Hook.memoizedState 以及 Hook.baseState。
注:其實單獨使用 useState 的話幾乎不會遇到 re-render 的場景,除非直接把 setState 寫在函數的頂部,但是這樣會導致無限 re-render,numberOfReRenders 會突破限制,在 4.3.4 dispatchAction 中讓程序報錯(4.3.4 隱去了
__DEV__
與這部分容錯代碼):
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
那麼再來看一下非 re-render 的情況,除去 Fiber 相關的代碼和特殊邏輯,重點在於 do-while 循環,這段代碼負責循環鏈表,執行每一次更新:
do {
// 循環鏈表,執行每一次更新
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
還有一點需要注意,在這種情況下需要對每一個 update 判斷優先級,如果不是當前整體更新優先級內的更新會被跳過,第一個跳過的 update 會變成新的 Hook.baseUpdate。需要保證後續的更新要在 baseUpdate 更新之後的基礎上再次執行,結果可能會不一樣。這裡的具體邏輯後續會成文單獨解析。最後同樣需要賦值給 Hook.memoizedState 以及 Hook.baseState。
4.4.3 updateWorkInProgressHook#
這裡補充一下,注意到第一行代碼獲取 Hook 的方式就與 mountState 不同,updateWorkInProgressHook 是獲取當前正在工作中的 Hook。實現如下:
// 獲取當前正在工作中的 Hook,即 workInProgressHook
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
這裡分為兩種情況,在 4.3.4 中我們提到如果 nextWorkInProgressHook 存在那麼就是 re-render,如果是 re-render 說明當前更新周期中還要繼續處理 workInProgressHook。
如果不是 re-render,就取下一個 Hook 為當前的 Hook,同時像 4.3.3 mountWorkInProgressHook 一樣,新建一個 Hook 並返回 workInProgressHook。
總之,updateWorkInProgressHook 獲取到了當前工作中的 workInProgressHook。
5 結語#
直觀一點,我截了個 Hook 在運行中的數據結構,如下圖所示:
總結一下上文中解析的流程,如下圖所示:
如果對於 useState 的源碼仍有所疑惑,可以自己寫個小 Demo 在關鍵函數打斷點調試一下。