React Hooks 源碼解析(譯)

咱們都知道,新的React Hook系統在社區中引發的反響很大。咱們已經嘗試和測試過,而且對它及其潛力感到十分興奮。當你想到hooks時會以爲他們很神奇,不暴露你的實例,React就能管理你的組件(不使用this關鍵字)。那麼React究竟是怎麼作到的呢?javascript

今天我將會深刻React hooks的實現來讓咱們更加了解它。這個神奇的特性存在的問題是,一旦出現問題就很難調試,由於它有複雜的堆棧跟蹤支持。所以,經過深刻理解React hooks的系統,咱們就能夠在遇到問題時很是快的解決它們,甚至能夠提早避免錯誤發生。java

在我開始以前,我首先要聲明我並非React的開發者/維護者,所以,你們不要太信任個人觀點。我確實很是深刻地研究了React hooks的實現,可是不管如何我也不能保證這就是hooks的實際實現原理。話雖如此,我已經用React源碼來支持個人觀點,並嘗試着使個人論點儘量的真實。node

image.png

首先,讓咱們進入須要確保hooks在React的做用域調用的機制,由於你如今可能知道若是在沒有正確的上下文調用鉤子是沒有意義的:react

The dispatcher

dispatcher 是包含了hooks函數的共享對象。它將根據ReactDom的渲染階段來動態分配或者清除,而且確保用戶沒法在 React 組件外訪問hooks。請參閱實現git

咱們能夠在渲染根組件前經過簡單的切換來使用正確的dispatcher,用一個叫作enableHooks的標誌來開啓/禁用;這意味這從技術上來講,咱們能夠在運行時開啓/禁用掛鉤。React 16.6.x就已經有了試驗性的實現,只不過它是被禁用的。請參閱實現github

當咱們執行完渲染工做時,咱們將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…

相關文章
相關標籤/搜索