本文首發於公衆號:符合預期的CoyPanjavascript
React Hook已經正式發佈了一段時間了。我在項目中也進行過嘗試,一個很直觀的感覺:寫起來很爽。可是一直沒有深刻了解過其實現原理。本文將嘗試從源碼層面,瞭解React hooks的原理。本文所指的React版本爲:v16.12.0html
Hook 是 React 16.8 的新增特性,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。Hook的誕生,是爲共享狀態邏輯提供更好的原生途徑。React官方文檔,已經對hook進行了十分全面的介紹:reactjs.org/docs/hooks-…java
Hook中使用的最多的,可能就是useState這個api。React是如何實現這個api的呢?React如何保存Hook的state?爲何咱們每次調用useState的時候,均可以拿到最新的值?react
在寫做文本前,我對React內部實現的瞭解並不夠深刻,只是瞭解其Fiber實現及工做流程。不過我認爲這對理解hook的實現原理並不會有很大的影響。api
本文將專一於這兩個問題。咱們經過示例代碼,調試一下react的源碼。示例代碼以下:函數
const App = () => {
const [name, setName] = useState('hello');
const [password, setPassword] = useState('world');
return <React.Fragment>
<input value={name} onChange={ e => setName(e.target.value) } />
<input value={password} onChange={ e => setPassword(e.target.value) } />
</React.Fragment>
};
複製代碼
useState的入口代碼:源碼分析
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
複製代碼
這個dispatcher對象包含了全部的官方內置hook。this
readContext: ƒ (context, observedBits)
useCallback: ƒ (callback, deps)
useContext: ƒ (context, observedBits)
useEffect: ƒ (create, deps)
useImperativeHandle: ƒ (ref, create, deps)
useLayoutEffect: ƒ (create, deps)
useMemo: ƒ (create, deps)
useReducer: ƒ (reducer, initialArg, init)
useRef: ƒ (initialValue)
useState: ƒ (initialState)
useDebugValue: ƒ (value, formatterFn)
useResponder: ƒ (responder, props)
useDeferredValue: ƒ (value, config)
useTransition: ƒ (config)
複製代碼
先暫時無論這個dispatcher是怎麼來的,咱們接着看下useState的內部邏輯。spa
React處理渲染帶有hook的組件的核心函數爲:renderWithHooks
。3d
咱們來看看再返回[name, setName]
以前,都經歷了哪些步驟:
...
// 組件掛載時,生成一個hook對象
var hook = mountWorkInProgressHook();
// 初始state綁在hook對象上
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 記錄hook值的改變
var queue = hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
// 生成 更改狀態的方法。
// 這裏的 currentlyRenderingFiber$1是一個全局變量,表示當前正在渲染的Fiber節點。這個很重要,一下子再說。
var dispatch = queue.dispatch = dispatchAction.bind(null,currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
...
複製代碼
首先來看hook對象是如何生成:
function mountWorkInProgressHook() {
// 初始化的hook對象
var hook = {
memoizedState: null, // 當前的state值
baseState: null,
queue: null,
baseUpdate: null,
next: null
};
// workInProgressHook是一個全局變量,表示當前正在處理的hook
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
複製代碼
從上面的代碼能夠看到,hook實際上是以鏈表的形式存儲起來的。每個hook都有一個指向下一個hook的指針。若是咱們在組件代碼中聲明瞭多個hook,那這些hook對象之間是這樣排列的:
hookA.next = hookB;
hookB.next = hookC;
...
複製代碼
React會把hook對象掛到Fiber節點的memoizedState
屬性上:
var renderedWork = currentlyRenderingFiber$1;
renderedWork.memoizedState = firstWorkInProgressHook;
複製代碼
在本文的例子中,組件掛載完畢後,其Fiber對象上的memoizedState
屬性值以下:
組件初次掛載完成了,咱們在頁面進行輸入時,hook是如何工做的呢?即,[name, setName]=useState('')
中的setName
都幹了什麼。
上文已經提到了,hook中的setName
是這樣來的
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
複製代碼
咱們調用setName
的時候,事實上就是執行了這個dispatchAction
。
在dispatchAction
中,會進行render的調度處理,同時,會存儲這次更新的信息:
...
// 記錄更新信息
var _update2 = {
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
...
// 這裏的queue,是以前傳入的hook對象中的queue。這裏保留了一個引用,很重要。
var last = queue.last;
// 更新鏈表
if (last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
last.next = _update2;
}
queue.last = _update2;
// 接下來,交給React去調度處理
...
複製代碼
通過React的調度,會帶上action(setName的傳參),再次進入hook組件核心渲染邏輯:renderWithHooks
。此時,因爲並不是首次渲染組件,React會使用另一個掛載有全局hook函數的對象上的useState
。在這個useState
中,會使用一個叫作updateState
的函數來計算最新的state值。而這個updateState
的代碼很簡單:
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
複製代碼
先看這個basicStateReducer
, 也很簡單:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
就是實現了一個reducer的功能,經過傳入的state的action,計算出最新的state。這裏的代碼告訴咱們,咱們其實能夠傳入一個函數做爲userState
的參數,經過函數返回最新的state。
咱們繼續深刻updateReducer
的代碼:
function updateReducer(reducer, initialArg, init) {
// 經過fiber節點的memoizedState屬性拿到當前hook。
var hook = updateWorkInProgressHook();
var queue = hook.queue;
queue.lastRenderedReducer = reducer;
// 這裏是爲了處理當前render週期中,再次觸發render的問題,先暫時忽略這個條件分支。
if (numberOfReRenders > 0) {
...
}
// 最近的一次更新
var last = queue.last;
var baseUpdate = hook.baseUpdate;
var baseState = hook.baseState; // Find the first unprocessed update.
var first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
var _newState = baseState;
var newBaseState = null;
var newBaseUpdate = null;
var prevUpdate = baseUpdate;
var _update = first;
var didSkip = false;
// 調度,獲取最新的state
do {
var updateExpirationTime = _update.expirationTime;
if (updateExpirationTime < renderExpirationTime$1) {
// react調度代碼,省略
...
} else {
...
if (_update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
_newState = _update.eagerState;
} else {
// 經過reducer計算出最新的state
var _action = _update.action;
_newState = reducer(_newState, _action);
}
}
prevUpdate = _update;
_update = _update.next;
} while (_update !== null && _update !== first);
...
hook.memoizedState = _newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = _newState;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
複製代碼
咱們以示例代碼,簡單分析了一下hook的內部原理。回到咱們開始提出的問題,React如何保存Hook的state?爲何咱們每次調用useState的時候,均可以拿到最新的值?如今能夠簡單總結一下了:
在執行useState
的時候,react會在組件的Fiber節點上,按照useState
的前後順序,以鏈表的方式建立hook,而且將state和該state對應的更新函數返回。更新時,會順着鏈表,依次計算最新的state值返回。
用一個圖總結一下:
上面的圖,也很好的解釋了爲何不容許在條件分支中使用hook。useState執行一次,鏈表纔會前進到下一個節點。若是中途某個節點斷了,那麼state就對應不起來了,代碼天然就出Bug了。
本文從源碼角度,闡述了hook的大概實現原理。其中有一些分支邏輯以及hook中的反作用等將在後續的文章中進行解析。本人第一次寫源碼分析,有不恰當的地方請多指教。