Deep into React Hooks
前言
在React 16.7 的版本中,Hooks 誕生了,截止到目前, 也有五六個月了, 想必大家也也慢慢熟悉了這個新名詞。
我也一樣, 對著這個新特性充滿了好奇, 也寫了幾個demo 體驗一下, 這個特性使得我們可以在一個函式元件中實現管理狀態, 可以說是十分的神奇。 樓主最近也看了一些這方面的文章, 在這裡總結分享一下, 希望對大家有所啟發。
Hooks 系統總覽
首先, 我們需要知道的是, 只有在 React scope 內呼叫的 Hooks 才是有效的,那 React 用什麼機制來保證 Hooks 是在正確的上下文被呼叫的呢?
Dispatcher
dispatcher 是一個包含了諸多 Hook functions 的共享物件,在 render phase,它會被自動的分配或者銷燬,它也保證 Hooks 不會在React component 之外被呼叫。
Hooks 功能的開啟和關閉由一個flag 控制,這意味著, 在執行時之中, 可以動態的開啟,關閉 Hooks相關功能。
React 16.6.X 也有一些試驗性的功能是通過這種方式控制的, 具體實現參考:
if (enableHooks) { ReactCurrentOwner.currentDispatcher = Dispatcher; } else { ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks; }
render 執行完畢之後,就銷燬dispatcher, 這樣也能組織在 react 渲染週期之外意外的呼叫Hooks.
對應原始碼:
// We're done performing work. Time to clean up. isWorking = false; ReactCurrentOwner.currentDispatcher = null; resetContextDependences(); resetHooks(); // Yield back to main thread.
Hooks 的執行是由一個叫 resolveDispatcher
的函式來決定的。 就像之前提到的, 在React 渲染週期之外 呼叫Hooks 是無效的, 這時候, React 也會跑出錯誤:
'Hooks can only be called inside the body of a function component.'
原始碼如下 :
function resolveDispatcher() { const dispatcher = ReactCurrentOwner.currentDispatcher; invariant( dispatcher !== null, 'Hooks can only be called inside the body of a function component.', ); return dispatcher; }
以上我們瞭解了Hooks的基礎機制, 下面我們再看幾個核心概念。
Hooks 佇列
我們都知道, Hooks 的呼叫順序十分重要。
React 假設當你多次呼叫 useState 的時候,你能保證每次渲染時它們的呼叫順序是不變的。
Hooks 不是獨立的,就好比是根據呼叫順序被串起來的一系列結點。
在瞭解這個機制之前,我們需要了解幾個概念:
- 在初次渲染的時候, Hooks會被賦予一個初始值。
- 這個值在執行時會被更新。
- React 會記住Hooks的狀態。
- React 給根據呼叫順序給你提供正確的state。
- React 會知道每個Hook具體屬於哪個Fiber。
用一個例子來解釋吧, 假設, 我們有一個狀態集:
{ foo: 'foo', bar: 'bar', baz: 'baz', }
處理Hooks的時候,會被處理成一個佇列, 每一個結點都是一個 state 的 model :
{ memoizedState: 'foo', next: { memoizedState: 'bar', next: { memoizedState: 'bar', next: null } } }
此處原始碼 :
function createHook(): Hook { return { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; }
在一個function Component 被渲染之前, 一個名為 prepareHooks
的方法會被呼叫, 在這個方法裡, 當前的Fiber 和 Hooks 佇列重的第一個結點會被儲存到一個全域性變數裡, 這樣, 下次呼叫 useXXX
的時候, React 就知道改執行哪個context了。
對應原始碼:
let currentlyRenderingFiber let workInProgressQueue let currentHook // Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123 function prepareHooks(recentFiber) { currentlyRenderingFiber = workInProgressFiber currentHook = recentFiber.memoizedState } // Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148 function finishHooks() { currentlyRenderingFiber.memoizedState = workInProgressHook currentlyRenderingFiber = null workInProgressHook = null currentHook = null } // Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115 function resolveCurrentlyRenderingFiber() { if (currentlyRenderingFiber) return currentlyRenderingFiber throw Error("Hooks can't be called") } // Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267 function createWorkInProgressHook() { workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook() currentHook = currentHook.next workInProgressHook } function useXXX() { const fiber = resolveCurrentlyRenderingFiber() const hook = createWorkInProgressHook() // ... } function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) { prepareHooks(recentFiber, workInProgressFiber) Component(props) finishHooks() }
更新結束後, 一個名為 finishHooks
的方法會被呼叫, Hooks 佇列中第一個結點的引用會被記錄在 memoizedState
變數裡, 這個變數是全域性的, 意味著可以在外部去訪問, 比如:
const ChildComponent = () => { useState('foo') useState('bar') useState('baz') return null } const ParentComponent = () => { const childFiberRef = useRef() useEffect(() => { let hookNode = childFiberRef.current.memoizedState assert(hookNode.memoizedState, 'foo') hookNode = hooksNode.next assert(hookNode.memoizedState, 'bar') hookNode = hooksNode.next assert(hookNode.memoizedState, 'baz') }) return ( <ChildComponent ref={childFiberRef} /> ) }
下面我們就拿最常見的Hook來具體分析。
State Hooks
比如:
const [count, setCount] = useState(0);
其實, useState
的背後,是 useReducer
, 它提供一個一個簡單的 預先定義的 reducer handler
。 原始碼實現
也就意味著, 我們通過 useState拿到的兩個值, 其實分別是 一個 reducer 的 state
, 和 一個 action 的 dispatcher
.
此處原始碼:
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
如程式碼所示, 我們可以直接提供一個 state 和對應的 action dispatcher。 但是與此同時, 我們也可以直接傳遞一個包含action 的dispatcher 進去, 接收一箇舊的state, 返回新的state.
這意味著我們可以把一個state的setter當作一個引數傳遞給Component, 然後在父元件裡修改state, 而不用傳遞一個新的prop進去。
簡單示例:
const ParentComponent = () => { const [name, setName] = useState() return ( <ChildComponent toUpperCase={setName} /> ) } const ChildComponent = (props) => { useEffect(() => { props.toUpperCase((state) => state.toUpperCase()) }, [true]) return null }
官網中也有類似的例子:
function Counter({initialCount}) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ); }
說完了State, 我們再看一下Effect。
Effect Hooks
Efftect 稍微有些不同, 它增加了額外的邏輯層。 在深入具體的實現之前, 我們需要事先了解幾點概念:
- Effect Hooks 在
render
的時候被建立, 在painting
之後被執行, 在下一次painting
之前被銷燬。 - Effect Hooks 按照定義的順序執行。
需要注意的一點是, painting
和 render
還是有所區別的,render method 只是建立了一個Fiber node, 還沒開始 paint.
// 腦坑疼, 休息一下再補充,未完待續...