React Hooks 原始碼解析(譯)

YardWill發表於2019-05-05

我們都知道,新的React Hook系統在社群中引起的反響很大。我們已經嘗試和測試過,並且對它及其潛力感到十分興奮。當你想到hooks時會覺得他們很神奇,不暴露你的例項,React就能管理你的元件(不使用this關鍵字)。那麼React到底是怎麼做到的呢?

今天我將會深入React hooks的實現來讓我們更加了解它。這個神奇的特性存在的問題是,一旦出現問題就很難除錯,因為它有複雜的堆疊跟蹤支援。因此,通過深入理解React hooks的系統,我們就可以在遇到問題時非常快的解決它們,甚至可以提前避免錯誤發生。

在我開始之前,我首先要宣告我並不是React的開發者/維護者,因此,大家不要太信任我的觀點。我確實非常深入地研究了React hooks的實現,但是無論如何我也不能保證這就是hooks的實際實現原理。話雖如此,我已經用React原始碼來支援我的觀點,並嘗試著使我的論點儘可能的真實。

image.png

首先,讓我們進入需要確保hooks在React的作用域呼叫的機制,因為你現在可能知道如果在沒有正確的上下文呼叫鉤子是沒有意義的:

The dispatcher

dispatcher 是包含了hooks函式的共享物件。它將根據ReactDom的渲染階段來動態分配或者清除,並且確保使用者無法在 React 元件外訪問hooks。請參閱實現

我們可以在渲染根元件前通過簡單的切換來使用正確的dispatcher,用一個叫做enableHooks的標誌來開啟/禁用;這意味這從技術上來說,我們可以在執行時開啟/禁用掛鉤。React 16.6.x就已經有了試驗性的實現,只不過它是被禁用的。請參閱實現

當我們執行完渲染工作時,我們將dispatcher 置空從而防止它在ReactDOM的渲染週期之外被意外呼叫。這是一種可以確保使用者不做傻事的機制。請參閱實現

dispatcher 在每一個 hook 呼叫中 使用resolveDispatcher()這個函式來呼叫。就像我之前說的,在React的渲染週期之外呼叫是毫無意義的,並且React會列印出警告資訊“Hooks只能在函式元件的主體內部呼叫”請參照實現

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。我想向您介紹一個新概念:

The hooks queue

在使用場景之後,hooks表示為在呼叫順序下連結在一起的節點。它們被表示成這樣是因為hooks並不是簡單的建立然後又把它遺留下來。它們有一種可以讓他們變成它們自己的機制。一個Hook有幾個我希望你可以在深入研究實現之前記住的屬性:

  1. 它的初始狀態在首次渲染時被建立。
  2. 她的狀態可以即時更新。
  3. React會在之後的渲染中記住hook的狀態
  4. React會根據呼叫順序為您提供正確的狀態
  5. React會知道這個hook屬於哪個Fiber。

因此,我們需要重新思考我們檢視元件狀態的方式。到目前為止,我們認為它就像是一個普通的物件:

{
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',
}
複製程式碼

但是在處理hook時,它應該被視為一個佇列,其中每個節點代表一個狀態的單個模型:

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}
複製程式碼

可以在實現中檢視單個hook節點的模式。你會看到hook有一些額外的屬性,但是理解鉤子如何工作的關鍵在於memoizedState和next。其餘屬性由useReducer()hook專門用於快取已經排程的操作和基本狀態,因此在各種情況下,還原過程可以作為後備重複: · baseState - 將給予reducer的狀態物件。 · baseUpdate- 最近的建立了最新baseState的排程操作。 · queue - 排程操作的佇列,等待進入reducer。

不幸的是,我沒有設法很好地掌握reducer hook,因為我沒有設法重現任何邊緣情況,所以我不覺得舒服去精心設計。我只能說,reducer 的實現是如此不一致,在程式碼註釋中甚至指出,“不知道這些是否都是所需的語義”; 所以我該如何確定?!

所以回到hooks,在每個函式元件呼叫之前,將呼叫一個名為prepareHooks()的函式,其中當前fiber及其hooks佇列中的第一個hook節點將被儲存在全域性變數中。這樣,只要我們呼叫一個hook函式(useXXX()),就會知道要在哪個上下文中執行。

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佇列中第一個節點的引用將儲存在渲染完成的fiber物件的memoizedState屬性中。這意味著hooks佇列及其狀態可以在外部被定位到:

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開始:

State hooks

你將驚訝的瞭解到useState hook使用的useReducer只是為它提供了一個預定義的reducer處理程式請參閱實現。這意味著實際上useState返回的結果是一個reducer狀態和一個action dispatcher。我希望你看一下state hook使用的reducer處理程式:

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
複製程式碼

正如預期的那樣,我們可以直接為action dispatcher提供新的狀態; 但你會看那個嗎?!我們還可以為dispatcher提供一個動作函式,該函式將接收舊狀態並返回新狀態。這意味著,當你將狀態設定器傳遞到子元件時,你可以改變當前父元件的狀態,不需要作為一個不同的prop傳遞下去。舉個例子:

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 的行為略有不同,並且有一個額外的邏輯層,我接下來會解釋。同樣,在我深入瞭解實現之前,我希望你能記住effect hooks的屬性:

  1. 它們是在渲染時建立的,但它們在繪製後執行。
  2. 它們將在下一次繪製之前被銷燬。
  3. 它們按照已經被定義的順序執行。

請注意,我使用的是“繪製”術語,而不是“渲染”。這兩個是不同的東西,我看到最近React Conf中的許多發言者使用了錯誤的術語!即使在官方的React文件中,他們也會說“在渲染螢幕之後”,在某種意義上應該更像“繪製”。render方法只建立fiber節點,但沒有繪製任何東西。

因此,應該有另一個額外的佇列保持這些effect,並應在繪製後處理。一般而言,fiber保持包含effect節點的佇列。每種effect都是不同的型別,應在適當的階段處理:

· 在變化之前呼叫例項的getSnapshotBeforeUpdate()方法請參閱實現。 ·執行所有節點的插入,更新,刪除和ref解除安裝操作請參閱實現。 ·執行所有生命週期和ref回撥。生命週期作為單獨的過程發生,因此整個樹中的所有放置,更新和刪除都已經被呼叫。此過程還會觸發任何特定渲染的初始effects請參閱實現。 ·由useEffect() hook 安排的effects - 基於實現也被稱為“passive effects” (也許我們應該在React社群中開始使用這個術語?!)。

當涉及到hook effects時,它們應該儲存在fiber的一個名為 updateQueue的屬性中,並且每個effect node應該具有以下模式請參閱實現

· tag - 一個二進位制數,它將決定effect的行為(我將盡快闡述)。 · create- 繪製後應該執行的回撥。 · destroy- 從create()返回的回撥應該在初始渲染之前執行。 · inputs - 一組值,用於確定是否應銷燬和重新建立effect。 · next - 函式元件中定義的下一個effect的引用。

除了tag屬性外,其他屬性都非常簡單易懂。如果你已經很好地研究了hooks,你就會知道React為你提供了幾個特殊的hooks:useMutationEffect()和useLayoutEffect()。這兩種效果在內部使用useEffect(),這實際上意味著它們建立了一個effect節點,但它們使用不同的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;
複製程式碼

這些二進位制值的最常見用例是使用管道(|)將這些位按原樣新增到單個值。然後我們可以使用&符號(&)檢查標籤是否實現某種行為。如果結果為非零,則表示tag實現了指定的行為。

以下是React支援的hook effect型別及其標籤請參閱實現

Default effect — UnmountPassive | MountPassive. Mutation effect — UnmountSnapshot | MountMutation. Layout effect — UnmountMutation | MountLayout. 以下是React如何檢查行為實現請參閱實現

if ((effect.tag & unmountTag) !== NoHookEffect) {
  // Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
  // Mount
}
複製程式碼

因此,基於我們剛剛學到的關於effect hooks的內容,我們實際上可以在外部向某個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} />
)
複製程式碼

就是這樣!你從這篇文章中最大的收穫是什麼?你將如何在你的React應用程式中使用這些新知識?很想看到有趣的評論!

medium.com/the-guild/u…

相關文章