- 原文地址:Under the hood of React’s hooks system
- 原文做者:Eytan Manor
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:sunui,hanxiansen
咱們將會一塊兒查看它的實現方法,由內而外地學習 React Hook。html
咱們都已經據說過了:React 16.7 的新特性,hook 系統,並已在社區中激起了熱議。咱們都試用過、測試過,對它自己和它的潛力都感到很是興奮。你必定認爲 hook 如魔法般神奇,React 居然能夠在不暴露實例的狀況下(不須要使用 this
關鍵字),幫助你管理組件。那麼 React 到底是怎麼作到的呢?前端
那麼今天,讓咱們一塊兒深刻探究 React Hook 的實現方法,以便更好的理解它。可是,它的各類神奇特性的不足是,一旦出現問題,調試很是困難,這是因爲它的背後是由複雜的堆棧追蹤(stack trace)支持的。所以,經過深刻學習 React 的新特性:hook 系統,咱們就能比較快地解決遇到的問題,甚至能夠直接杜絕問題的發生。react
在開始講解以前,我先聲明我不是 React 的開發者或者維護者,因此個人理解可能也並非徹底正確。我確實很是深刻地研究過了 React 的 hook 系統的實現,可是不管如何我仍沒法保證這就是 React 實際的工做方式。話雖如此,我仍是會用 React 源代碼中的證據和引用來支持個人文章,使個人論點儘量堅實。android
React hook 系統概要示意圖ios
咱們先來了解 hook 的運行機制,並要確保它必定在 React 的做用域內使用,由於若是 hook 不在正確的上下文中被調用,它就是毫無心義的,這一點你或許已經知道了。git
dispatcher 是一個包含了 hook 函數的共享對象。基於 ReactDOM 的渲染狀態,它將會被動態的分配或者清理,而且它可以確保用戶不可在 React 組件以外獲取 hook(詳見源碼)。github
在切換到正確的 Dispatcher 以渲染根組件以前,咱們經過一個名爲 enableHooks
的標誌來啓用/禁用 hook。在技術上來講,這就意味着咱們能夠在運行時開啓或關閉 hook。React 16.6.X 版本中也有對此的實驗性實現,但它實際上處於禁用狀態(詳見源碼)json
當咱們完成渲染工做後,咱們將 dispatcher 置空並禁止用戶在 ReactDOM 的渲染週期以外使用 hook。這個機制可以保證用戶不會作什麼蠢事(詳見源碼)。後端
dispatcher 在每次 hook 的調用中都會被函數 resolveDispatcher()
解析。正如我以前所說,在 React 的渲染週期以外,這些都無心義了,React 將會打印出警告信息:「hook 只能在函數組件內部調用」(詳見源碼)。設計模式
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
}
複製代碼
dispatcher 實現方式概覽。
如今咱們簡單瞭解了 dispatcher 的封裝機制,下面繼續回到本文的核心 —— hook。下面我想先給你介紹一個新的概念:
在 React 後臺,hook 被表示爲以調用順序鏈接起來的節點。這樣作緣由是 hook 並不能簡單的被建立而後丟棄。它們有一套特有的機制,也正是這些機制讓它們成爲 hook。一個 hook 會有數個屬性,在繼續學習以前,我但願你能牢記於心:
另外,咱們也須要從新思考看待組件狀態的方式。目前,咱們只把它看做一個簡單的對象:
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
}
複製代碼
舊視角理解 React 的狀態
可是當處理 hook 的時候,狀態須要被看做是一個隊列,每一個節點都表示一個狀態模型:
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'bar',
next: null
}
}
}
複製代碼
新視角理解 React 的狀態
單個 hook 節點的結構能夠在源碼中查看。你將會發現,hook 還有一些附加的屬性,可是弄明白 hook 是如何運行的關鍵在於它的 memoizedState
和 next
屬性。其餘的屬性會被 useReducer()
hook 使用,能夠緩存發送過的 action 和一些基本的狀態,這樣在某些狀況下,reduction 過程還能夠做爲後備被重複一次:
baseState
—— 傳遞給 reducer 的狀態對象。baseUpdate
—— 最近一次建立 baseState
的已發送的 action。queue
—— 已發送 action 組成的隊列,等待傳入 reducer。不幸的是,我尚未徹底掌握 reducer 的 hook,由於我沒辦法復現它任何的邊緣狀況,因此講述這部分就很困難。我只能說,reducer 的實現和其餘部分相比顯得很不一致,甚至它本身源碼中的註解都聲明「不肯定這些是不是所須要的語義」;因此我怎麼可能肯定呢?!
因此咱們仍是回到對 hook 的討論,在每一個函數組件調用前,一個名爲 [prepareHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123)
的函數將先被調用,在這個函數中,當前 fiber 和 fiber 的 hook 隊列中的第一個 hook 節點將被保存在全局變量中。這樣,咱們不管什麼時候調用 hook 函數(useXXX()
),它都能知道運行上下文。
let currentlyRenderingFiber
let workInProgressQueue
let currentHook
// 源代碼:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
currentlyRenderingFiber = workInProgressFiber
currentHook = recentFiber.memoizedState
}
// 源代碼:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
currentlyRenderingFiber.memoizedState = workInProgressHook
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// 源代碼: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")
}
// 源代碼: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()
}
複製代碼
hook 隊列實現的概覽。
一旦更新完成,一個名爲 [finishHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148)
的函數將會被調用,在這個函數中,hook 隊列中第一個節點的引用將會被保存在已渲染 fiber 的 memoizedState
屬性中。這就意味着,hook 隊列和它的狀態能夠在外部定位到。
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 hook:
你必定會以爲很吃驚:useState
hook 在後臺使用了 useReducer
,而且它將 useReducer
做爲預約義的 reducer(詳見源碼)。這意味着,useState
返回的結果實際上已是 reducer 狀態,同時也是一個 action dispatcher。請看,以下是 state hook 使用的 reducer 處理器:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
state hook 的 reducer,又名基礎狀態 reducer。
因此正如你想象的那樣,咱們能夠直接將新的狀態傳入 action dispatcher;可是你看到了嗎?!咱們也能夠傳入 action 函數給 dispatcher,這個 action 函數能夠接收舊的狀態並返回新的。(在本篇文章寫就時,這種方法並無記錄在 React 官方文檔中,很遺憾的是,它其實很是有用!)這意味着,當你向組件樹發送狀態設置器的時候,你能夠修改父級組件的狀態,同時不用將它做爲另外一個屬性傳入,例如:
const ParentComponent = () => {
const [name, setName] = useState()
return (
<ChildComponent toUpperCase={setName} /> ) } const ChildComponent = (props) => { useEffect(() => { props.toUpperCase((state) => state.toUpperCase()) }, [true]) return null } 複製代碼
根據舊狀態返回新狀態。
最後,effect hook —— 它對於組件的生命週期影響很大,那麼它是如何工做的呢:
effect hook 和其餘 hook 的行爲有一些區別,而且它有一個附加的邏輯層,這點我在後文將會解釋。在我分析源碼以前,首先我但願你牢記 effect hook 的一些屬性:
注意,我使用了「繪製」而不是「渲染」。它們是不一樣的,在最近的 React 會議中,我看到不少發言者錯誤的使用了這兩個詞!甚至在官方 React 文檔中,也有寫「在渲染生效於屏幕以後」,其實這個過程更像是「繪製」。渲染函數只是建立了 fiber 節點,可是並無繪製任何內容。
因而就應該有另外一個隊列來保存這些 effect hook,而且還要可以在繪製後被定位到。一般來講,應該是 fiber 保存包含了 effect 節點的隊列。每一個 effect 節點都是一個不一樣的類型,並能在適當的狀態下被定位到:
在修改以前調用 getSnapshotBeforeUpdate()
實例(詳見源碼)。
運行全部插入、更新、刪除和 ref 的卸載(詳見源碼)。
運行全部生命週期函數和 ref 回調函數。生命週期函數會在一個獨立的通道中運行,因此整個組件樹中全部的替換、更新、刪除都會被調用。這個過程還會觸發任何特定於渲染器的初始 effect hook(詳見源碼)。
useEffect()
hook 調度的 effect —— 也被稱爲「被動 effect」,它基於這部分代碼(也許咱們要開始在 React 社區內使用這個術語了?!)。
hook effect 將會被保存在 fiber 一個稱爲 updateQueue
的屬性上,每一個 effect 節點都有以下的結構(詳見源碼):
tag
—— 一個二進制數字,它控制了 effect 節點的行爲(後文我將詳細說明)。create
—— 繪製以後運行的回調函數。destroy
—— 它是 create()
返回的回調函數,將會在初始渲染前運行。inputs
—— 一個集合,該集合中的值將會決定一個 effect 節點是否應該被銷燬或者從新建立。next
—— 它指向下一個定義在函數組件中的 effect 節點。除了 tag
屬性,其餘的屬性都很簡明易懂。若是你對 hook 很瞭解,你應該知道,React 提供了一些特殊的 effect hook:好比 useMutationEffect()
和 useLayoutEffect()
。這兩個 effect hook 內部都使用了 useEffect()
,實際上這就意味着它們建立了 effect hook,可是卻使用了不一樣的 tag 屬性值。
這個 tag 屬性值是由二進制的值組合而成(詳見源碼):
const NoEffect = /* */ 0b00000000;
const UnmountSnapshot = /* */ 0b00000010;
const UnmountMutation = /* */ 0b00000100;
const MountMutation = /* */ 0b00001000;
const UnmountLayout = /* */ 0b00010000;
const MountLayout = /* */ 0b00100000;
const MountPassive = /* */ 0b01000000;
const UnmountPassive = /* */ 0b10000000;
複製代碼
React 支持的 hook effect 類型
這些二進制值中最經常使用的情景是使用管道符號(|
)鏈接,將比特相加到單個某值上。而後咱們就可使用符號(&
)檢查某個 tag 屬性是否能觸發一個特定的行爲。若是結果是非零的,就表示能夠。
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
複製代碼
如何使用 React 的二進制設計模式的示例
這裏是 React 支持的 hook effect,以及它們的 tag 屬性(詳見源碼):
UnmountPassive | MountPassive
.UnmountSnapshot | MountMutation
.UnmountMutation | MountLayout
.以及這裏是 React 如何檢查行爲觸發的(詳見源碼):
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
複製代碼
React 源碼節選
因此,基於咱們剛纔學習的關於 effect hook 的知識,咱們能夠實際操做,從外部向 fiber 插入一些 effect:
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} /> ) 複製代碼
插入 effect 的示例
這就是 hooks 了!閱讀本文你最大的收穫是什麼?你將如何把新學到的知識應用於 React 應用中?但願看到你留下有趣的評論!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。