原文:medium.com/the-guild/u…javascript
對於 React 16.7 中新的 hooks 系統在社區中引發的騷動,咱們都有所耳聞了。人們紛紛動手嘗試,併爲之興奮不已。一想到 hooks 時它們彷佛是某種魔法,React 以某種甚至不用暴露其實例(起碼沒有用到這個關鍵詞)的手段管理了你的組件。那麼 React 究竟搗了什麼鬼呢?java
今天讓咱們來深刻 React 關於 hooks 的實現以更好地理解它。這個魔法特性的問題就在於一旦其發生了問題是難以調試的,由於它隱藏在了一個複雜的堆棧追蹤的背後。所以,深刻理解 React 的 hooks 系統,咱們就能在遭遇它們時至關快地解決問題,或至少能在早期階段避免它們。node
醜話說在前面,我並非一名 React 的開發者/維護者,以及個人言論不須要太過當真。我很是深刻的研究了 React 的 hooks 系統的實現,但無論怎麼說我也不能保證這就是 React 如何工做的真諦。也就是說,個人言論基於 React 的源碼,並儘量地讓個人論據可靠。react
首先,讓咱們瞭解一遍確保 hooks 在 React 的做用域內被調用的機制,由於你大概已經知道若是不在正確的上下文中調用,hooks 是沒有意義的:git
dispatcher 是一個包含了 hooks 函數的共享對象。它將基於 ReactDOM 的渲染階段被動態地分配或清理,而且它將確保用戶不會超出一個 React 組件去訪問 hooks (github.com/facebook/re…)。github
hooks 被一個叫作 enableHooks
的標誌位變量啓用或禁用,在咱們剛剛渲染根組件時,判斷該標誌位並簡單的切換到合適的 dispatcher 上;這意味着從技術上來講咱們能在運行時啓用或禁用 hooks。React 16.6.X 也試驗性的實現了該特性, 但其實是被禁用的 (github.com/facebook/re…).數組
當咱們完成了渲染工做,咱們將 dispatcher 做廢,這預防了 hooks 被意外地從 ReactDOM 的渲染循環以外訪問。該機制將確保用戶不出昏招 (github.com/facebook/re…)。緩存
在全部 hook 的每一次調用時,都會用 resolveDispatcher()
得到 dispatcher 的引用。正如我以前所說,在 React 渲染循環以外的訪問應該是沒有意義的,這種狀況下 React 應該打印警告信息:「Hooks can only be called inside the body of a function component」 (github.com/facebook/re…)。ide
//react-hooks-dispatcher.js
let currentDispatcher
const dispatcherWithoutHooks = { /* ... */ }
const dispatcherWithHooks = { /* ... */ }
function resolveDispatcher() {
if (currentDispatcher) return currentDispatcher
throw Error("Hooks can't be called")
}
function useXXX(...args) {
const dispatcher = resolveDispatcher()
return dispatcher.useXXX(...args)
}
function renderRoot() {
currentDispatcher = enableHooks ? dispatcherWithHooks : dispatcherWithoutHooks
performWork()
currentDispatcher = null
}
複製代碼
咱們瞭解了這個簡單的封裝機制,讓咱們移向本文的核心 -- hooks。立刻爲你介紹一個新概念:函數
在帷幕以後,hooks 表現爲以其調用順序被連接在一塊兒的節點(nodes)。它們之因此表現成這樣是由於 hooks 並不是被簡單的建立後就獨自行事了。有一個容許它們按身份行事的機制。我想請你在深刻其實現以前記住一個 hook 的若干屬性:
相應的,咱們須要從新思考咱們看待一個組件的狀態的方式了。至今爲止咱們是將其看成一個 plain object 的:
//react-state-old.js
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
}
複製代碼
但當咱們處理 hooks 時應將其視做一個隊列,其每一個節點都表現爲一個單個的狀態模型:
//react-state-new.js
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'baz',
next: null
}
}
}
複製代碼
單個 hook 節點的模式能夠在實現中看到。你將發現 hook 有一些附加的屬性,但理解 hooks 如何工做的關鍵就潛藏在 memoizedState 和 next 中。其他的屬性被 useReducer() hook 特別的用來緩存已分發過的 actions 和基礎狀態,這樣在 useReducer 的遍歷過程當中相關邏輯就能夠在各類狀況下做爲一個 fallback 被重複執行:
baseState
: 會被傳給 reducer 的狀態對象baseUpdate
: 最近一次 dispatch 過的用來建立 baseState 的 actionqueue
: 一個 dispatch 過的 actions 列表,等待遍歷 reducer糟糕的是我沒法全面領悟 reducer hook,由於我沒能設法復現幾乎任何一個它的邊緣狀況,因此也就不展開細說了。我只能說 reducer 的實現是如此的先後矛盾以致於其本身的 一處註釋(github.com/facebook/re…) 中甚至說 「TODO: 不肯定這是否是預期的語義...我不記得是爲何了」;因此我又能如何肯定呢?!
回到 hooks,在每一個函數組件調用以前,一個叫作 prepareHooks()
的函數先被調用,當前 fiber 和其位於 hooks 隊列中的首個 hook 會被存儲在全局變量中。經過這種方式,每次咱們調用一個 hook 函數(useXXX()
)時,它都知道在哪一個上下文中運行了。
//react-hooks-queue.js
let currentlyRenderingFiber
let workInProgressQueue
let currentHook
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L123
function prepareHooks(recentFiber) {
currentlyRenderingFiber = workInProgressFiber
currentHook = recentFiber.memoizedState
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L148
function finishHooks() {
currentlyRenderingFiber.memoizedState = workInProgressHook
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L115
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#L267
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 隊列中首個節點的引用將被存儲在已渲染的 fiber 的 memoizedState
屬性中。這意味着 hooks 隊列和它們的狀態可被從外部處理:
//react-state-external.js
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} /> ) } 複製代碼
讓咱們看看更多的細節並談談個別 hooks,從最經常使用的 state hook 開始:
你知道了可能會驚訝,但 useState
hook 在幕後使用了 useReducer
並簡單地提供給後者一個預約義的 reducer 處理函數。這意味着從 useState
返回的結果其實是一個 reducer state 以及一個 action dispatcher。我想讓你看看 state hook 使用的 reducer 處理函數:
//react-basic-state-reducer.js
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
因此按照預期,咱們能夠向 action dispatcher 直接傳入新的 state;但你看到了什麼?!咱們也能傳入一個 action 函數,用以處理舊 state 並返回一個新的。這在官方文檔中從未說起(在本文成文之際)而且這有點遺憾由於這特別有用!這意味着當你已經把 state setter 發往組件樹後仍可改變父組件的當前狀態,而不用向其傳入一個不一樣的 prop。好比:
//react-state-dispatcher.js
const ParentComponent = () => {
const [name, setName] = useState()
return (
<ChildComponent toUpperCase={setName} /> ) } const ChildComponent = (props) => { useEffect(() => { props.toUpperCase((state) => state.toUpperCase()) }, [true]) return null } 複製代碼
最後來看看在一個組件的生命週期上施展魔法效果的 — effect hooks,以及它是如何工做的:
Effect hooks 表現得稍有不一樣,我也想說說其額外的一個邏輯層。再說一次,在我深刻解釋實現以前,但願你記住關於 effect hooks 屬性的一些事情:
注意我使用了術語 「painting」 而不是 「rendering」。這二者大相徑庭,而我注意到最近許多演說者最近在 React Conf (conf.reactjs.org/) 上使用了錯誤的詞語!甚至在官方 React 文檔中他們也說 「after the render is committed to the screen」,其實應該是相似 「painting」 的。render() 方法只是建立 fiber 節點但並不繪製任何東西。
相應地,也應該有另外一個額外的隊列來保存這些 effects 並能在繪製後被處理。通常來講,一個 fiber 持有一個包含了 effect 節點的隊列。每一個 effect 都屬於一個不一樣的類型並應該在其相應的階段被處理:
getSnapshotBeforeUpdate()
的實例useEffect()
hook 調度的 effects -- 從源碼中可知其稱呼爲 「passive effects(消極影響)」 (咱們或許應該在 React 社區中開始用這個術語了?!)hook effects 應該被存儲在 fiber 的 updateQueue
屬性上,而且每一個 effect 節點應該有以下結構:
tag
:一個二進制數字,表示該 effect 的行爲(稍後我會詳述)create
:繪製以後應該運行的回調destroy
:由 create()
回調返回,應該早於初次渲染運行inputs
: 一個值的集合,用來決定 effect 是否應該被銷燬或重建next
:一個對定義在函數組件中的下一個 effect 的引用除了 tag
屬性,其餘屬性都很易於理解。若是你熟悉 hooks,應該知道 React 提供了一對特殊的 effect hooks:useMutationEffect()
和 useLayoutEffect()
。二者內部都用了 useEffect()
,意味着本質上它們都建立了一個 effect 節點,但它們用了不一樣的 tag 值。
tag 由一組二進制值構成:
//react-hook-effects-types.js
const NoEffect = /* */ 0b00000000;
const UnmountSnapshot = /* */ 0b00000010;
const UnmountMutation = /* */ 0b00000100;
const MountMutation = /* */ 0b00001000;
const UnmountLayout = /* */ 0b00010000;
const MountLayout = /* */ 0b00100000;
const MountPassive = /* */ 0b01000000;
const UnmountPassive = /* */ 0b10000000;
複製代碼
對於這些二進制值最多見的用例會是使用一個通道操做(|
)並像單獨的值同樣增長二進制位。然後咱們就可使用一個 &
符號檢查一個 tag 是否實現了一個特定的行爲。若是結果非零,就意味着 tag 的實現達到了預期。
//react-bin-design-pattern-test.js
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
複製代碼
這是被 React 支持的 hook effect 類型,以及其 tags:
而且 React 是這樣檢查行爲實現的:
//react-effect-hooks-real-usage.js
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
複製代碼
因此,基於咱們以及學過的涉及 effect hooks 的知識,實際上能夠從外部向一個特定 fiber 注入一個 effect:
//react-hook-effect-injection.js
function injectEffect(fiber) {
const lastEffect = fiber.updateQueue.lastEffect
const destroyEffect = () => {
console.log('on destroy')
}
const createEffect = () => {
console.log('on create')
return destroy
}
const injectedEffect = {
tag: 0b11000000,
next: lastEffect.next,
create: createEffect,
destroy: destroyEffect,
inputs: [createEffect],
}
lastEffect.next = injectedEffect
}
const ParentComponent = (
<ChildComponent ref={injectEffect} /> ) 複製代碼
大功告成!
搜索 fewelife 關注公衆號
轉載請註明出處