React@16.8.6原理淺析(hooks)

本系列文章總共三篇:javascript

課前小問題

  1. hooks 是如何存儲狀態的
  2. 有多個相同的 hooks 時 react 是如何區分的

定義

React hooks api 是在 react 這個庫裏面定義的,咱們以 useState 爲例:java

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製代碼

咱們能夠發現 hooks 的定義很是簡單,只是獲取了 dispatch 而後調用 dispatcher 對應的 useState 屬性,其它 hooks 也是相似,好比 useEffect 是調用 dispatcher 的 useEffect 屬性。react

接着咱們就須要看看 dispatcher 究竟是什麼,經過查看 resolveDispatcher 咱們發現 dispatcher 指向的是 ReactCurrentDispatcher.currentgit

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    ' one of the following reasons:\n' +
    '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    '2. You might be breaking the Rules of Hooks\n' +
    '3. You might have more than one copy of React in the same app\n' +
    'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}
複製代碼

經過全局搜索咱們發現 **ReactCurrentDispatcher.current **在 ReactFiberHooks.js 這個文件中被賦值,接下來咱們就來看看這個文件。github

renderWithHooks

前言

通過搜索咱們發現 **ReactCurrentDispatcher.current **在 ReactFiberHooks.js 文件中被頻繁賦值,其中最主要被賦值的地方就在 renderWithHooks 方法中,通過搜索我發現 renderWithHooks 在 ReactFiberBeginWork.js 這個文件中被屢次調用,若是你以前看過上一篇文檔或是對 react 的更新流程的源碼比較熟悉的話,你應該知道 ReactFiberBeginWork.js 文件對應着 beginWork 這個方法,在這個方法中會找出要更新的 fiber 對象並執行對應的更新方法。
通過搜索我找到了和 function component 相關的幾個方法:updateFunctionComponent 和 mountIndeterminateComponent,這兩個都是更新 function component,區別是第一次渲染的時候會調用 mountIndeterminateComponent,由於第一次還沒法肯定是 function component 仍是 class component。api

mountIndeterminateComponent:
app

image.png

updateFunctionComponent:
ide

image.png

接下來咱們就來看看 renderWithHooks 到底作了什麼。函數

流程圖

具體邏輯

經過上面的流程圖,咱們發現 renderWithHooks 作了以下幾件事:post

  1. 經過判斷 nextCurrentHook 是否爲 null 來判斷是不是初次渲染,若是是初次渲染就將 ReactCurrentDispatcher.current 賦值爲 HooksDispatcherOnMount 不然賦值爲 HooksDispatcherOnUpdate
  2. 而後調用 function component 獲得 children
  3. 判斷是否存在嵌套更新(didScheduleRenderPhaseUpdate),若是存在就繼續執行第二步,直到嵌套更新結束或是超過最大嵌套更新層數
  4. 設置當前 fiber 對象上的 memoizedState 爲當前的 hook 對象,以及設置其它屬性,並將 effectTag 標記爲 sideEffectTag
  5. 重置全局變量
  6. 返回 children

HooksDispatcherOnMount

簡介

HooksDispatcherOnMount 對象中定義了各個 hooks api 在初次渲染中的實現

image.png

流程圖

HooksDispatcherOnUpdate

簡介

HooksDispatcherOnUpdate 對象中定義了各個 hooks api 在再次渲染中的實現

image.png

流程圖

useState

通過前面的講述此時你應該知道 useState 最終調用的是 ReactCurrentDispatcher.current.useState 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被賦值爲 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那咱們先來看一下 HooksDispatcherOnMount 中的實現。

mountState

  1. 首先調用 mountWorkInProgressHook 方法建立 hook 對象
  2. 判斷傳入的 initialState 也就是 useState 傳入的參數是不是函數,若是是就執行它獲得初始 state
  3. 設置 hook.memoizedState 和 hook.baseState 爲 initialState,這裏你就能夠知道爲何 function component 使用了 hook 以後就能夠保存狀態了,由於狀態保存在 hook 對象上了,而 hook 對象又保存在 fiber 對象的 memoizedState 屬性上
  4. 建立 queue 對象並賦值給 hook.queue,queue 相似於 fiber 對象上面的 updateQueue
  5. 爲將當前 fiber(workInProgress)和 queue 綁定爲 dispatchAction 的前兩個參數,並賦值給 dispatch
  6. 返回 [hook.memoizedState, dispatch]

updateState

updateState 內部調用了 updateReducer,updateRecucer 內部作了如下事情:

  1. 首先調用 mountWorkInProgressHook 方法建立 hook 對象
  2. 賦值 queue.lastRenderedReducer 爲 basicStateReducer
  3. 若是出現重複渲染(即在一次渲染中又調用了一次渲染),咱們去 renderPhaseUpdates 中根據 queue 獲取 update 而後遍歷執行 update 鏈表獲取 newState,而後判斷 newState 和 oldState 是否相等,若是不相等就標記更新,最後返回 [newState, dispatch]
  4. 若是沒有出現重複渲染就從 queue 找到最後一個 update,進而找到第一個 udpate,由於是循環鏈表因此能夠經過 last.next 找到 first,而後和第四步同樣循環執行 update 鏈表獲取 newState,而後判斷 newState 和 oldState 是否相等,若是不相等就標記更新,最後返回 [newState, dispatch]

dispatchAction

dispatchAction 就是 useState 返回的第二個參數

流程圖

具體邏輯

  1. 首先判斷一下是不是處於一個渲染階段的更新,若是是將 didScheduleRenderPhaseUpdate 設置爲 true,這個標誌位在 renderWithHooks 中被用於判斷是否處於嵌套更新,接着建立一個 update 對象,再建立一個 renderPhaseUpdates Map 對象,並以 queue 爲 key update 爲 value 存儲到 renderPhaseUpdate 中,renderPhaseUpdate 在 updateState 方法中會調用
  2. 若是不是處於一個渲染階段的更新,則先計算出 expirationTime 而後建立一個 update 對象,接着將 update 放到 queue.last 這個循環鏈表中,接着判斷一下若是當前 fiber.expirationTime = NoWork,而且 queue.lastRenderedReducer 不爲空,咱們就能夠經過 lastRenderedReducer 計算出新的 state(eagerState),lastRenderedReducer 接受以前的 state(currentState)和 action(就是傳入 useState 返回的第二個方法的參數),接着將 lastRenderedReducer 和 eagerState 賦值給 update 的 eagerReducer 和 eagerState,接着判斷新的 state (eagerState)和老的 state(currentState)是否相等,若是相等就直接 return 由於沒有更新產生,若是不相等那就調用 scheduleWork 進入調度階段,這個就和上一篇講的流程鏈接起來了。

本章解決的問題

  1. hooks 是如何存儲狀態的

UseEffect

通過前面的講述此時你應該知道 useEffect 和 useState 同樣,最終調用的是 ReactCurrentDispatcher.current.useEffect 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被賦值爲 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那咱們先來看一下 HooksDispatcherOnMount 中的實現。


mountEffect

HooksDispatcherOnMount 中 useEffect 指向的是 mountEffect,它又調用了 mountEffectImpl

function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}
複製代碼


mountEffectImpl

mountEffectImpl 作了如下事情:

  1. 經過 mountWorkInProgressHook 建立一個 hook 對象
  2. 將傳入的 fiberEffectTag 設置到 sideEffectTag 上,對應到 mountEffect 就是 UpdateEffect | PassiveEffect,最終 sideEffectTag 會被設置到當前 fiber 對象的 effectTag 上(參見 renderWithHooks
  3. 最後調用 pushEffect,傳入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps
  4. 將 pushEffect 的結果賦值給 hook.memoizedState

updateEffect

在更新階段會將 dispatcher 指向 HooksDispatcherOnUpdate,在 HooksDispatcherOnUpdate 中 useEffect 指向的是 updateEffect,它又調用了 updateEffectImpl。

function updateEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}
複製代碼

updateEffectImpl

updateEffectImpl 作了如下事情:

  1. 經過 updateWorkInProgressHook 建立一個 hook 對象
  2. 判斷 currentHook 是否爲 null,currentHook 不爲 null 說明不是初次渲染,獲取 currentHook.memoizedState,也就是上一個 effect 對象,找到該對象的 destory 屬性和 deps 屬性,判斷新的 deps 和老的 deps 是否相等,若是相等就調用 pushEffect 傳入 NoHookEffect,表示沒有 effect 須要執行,也就不會在 commit 階段執行 unmount 和 mount,也就是調用 destroy 和 create 方法,而後 return
  3. 若是 currentHook 等於 null 或是新的 deps 和老的 deps 不相等,將傳入的 fiberEffectTag 設置到 sideEffectTag 上(UpdateEffect | PassiveEffect),最終 sideEffectTag 會被設置到當前 fiber 對象的 effectTag 上(參見 renderWithHooks),最後調用 pushEffect,傳入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps,將 pushEffect 的結果賦值給 hook.memoizedState

pushEffect

  1. 建立一個 effect 對象
  2. 將 effect 添加到 componentUpdateQueue.lastEffect 上,造成一個循環鏈表,componentUpdateQueue 會被添加到當前 fiber 對象的 updateQueue 上(參見 renderWithHooks
  3. 返回 effect

effect

const effect: Effect = {
    tag, // hookEffectTag
    create, // useEffect 接收的第一個參數
    destroy, // 在 mountEffect 中是 undefined
    deps, // useEffect 接收的第二個參數
    // Circular
    next: (null: any), // 指向下一個 effect
  };
複製代碼

commitLayoutEffects

最終生成的 updateQueue 會在 commit 階段的 commitLayoutEffects 中執行
詳情能夠看上一篇

commitLayoutEffectOnFiber(commitLifeCycles)

還記得上面 mountEffectImpl 方法會將 UpdateEffect | PassiveEffect 設置到 fiber.effectTag 上,對於有 UpdateEffect 的 fiber 對象在 commitLayoutEffects 中會執行 commitLayoutEffectOnFiber 方法,它對應的就是  commitLifeCycles 方法,在該方法中對於 FunctionComponent 會執行 commitHookEffectList方法,傳入 UnmountLayout, MountLayout, finishedWork

commitHookEffectList

在該方法中會對傳入的 finishedWork.updateQueue 上面的 effect 對象執行 unmount 和 mount,也就是調用 effect 對象上的 destroy 方法和 create 方法,對應於 useEffect 返回的方法和傳入的方法,第一次渲染設置的 destroy 爲 undefined 因此第一次渲染 destroy 不會執行

useRef

useRef 和其它 hooks 同樣最終調用的是 ReactCurrentDispatcher.current.useRef 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被賦值爲 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那咱們先來看一下 HooksDispatcherOnMount 中的實現。


mountRef

在 HooksDispatcherOnMount 中 useRef 指向的是 mountRef 方法,咱們來看一下它作了什麼:

  1. 經過 mountWorkInProgressHook 方法建立了 hook 對象
  2. 建立 ref 對象 const ref = { current: initialValue }; 初始值就是傳入 useRef 的第一個參數
  3. 設置 hook.memoizedState = ref;
  4. 返回 ref

updateRef

在 HooksDispatcherOnUpdate 中 useRef 指向的是 updateRef 方法,咱們來看一下它作了什麼:

  1. 經過 updateWorkInProgressHook 獲取到 hook
  2. 返回 hook 對象上的 memoizedState

建立 hook

通過上面的幾個 hook api 的實現咱們發現每一個 hook api 都須要先建立一個 hook 對象,而建立 hook 對象針對初次渲染和再次渲染這兩個階段調用的方法有所不一樣,咱們先來看初次渲染。

mountWorkInProgressHook

初次渲染調用的是 mountWorkInProgressHook 方法,咱們來看一下它作了什麼:

  1. 建立一個 hook 對象
  2. 判斷 workInProgressHook 是否爲空,若是爲空就將 workInProgressHook 和 firstWorkInProgressHook 指向新的 hook
  3. 若是不爲空就插入其後(next),而後將 workInProgress 指向新的 hook
  4. 返回 workInProgress

updateWorkInProgressHook

接下來咱們看看再次渲染時調用的 updateWorkInProgressHook 方法:

  1. 首先判斷一下 nextWorkInProgressHook 是否爲空,若是不爲空說明當前處於渲染階段觸發的從新渲染,由於只有在從新渲染時 renderWithHooks 纔會將其設置爲 firstWorkInProgressHook,若是爲空就將 workInProgressHook 設置爲 nextWorkInProgressHook,而後將 nextWorkInProgressHook 設置爲 workInProgressHook.next,而後設置 nextCurrentHook
  2. 若是 nextWorkInProgressHook 爲空,咱們將 currentHook 設置爲 nextCurrentHook,也就是找到上一次渲染的 hook 對象(相似於 fiber裏面的 current),而後根據 currentHook 複製一個 newHook,執行 mountWorkInProgressHook 中的第二三步,而後將 nextCurrentHook 指向 currentHook 的 next,這裏咱們就能夠知道爲何多個 hook api 執行的時候 react 是如何一一對應的了,就是經過初次渲染造成的鏈表去對應的,因此千萬要注意先後兩次的渲染中 hook 的順序不能有改變
  3. 返回 workInProgress

hook

咱們來看看 hook 對象究竟是個什麼東西

const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };
複製代碼

memoizedState

存儲 hook 對象的數據,useState 對應的就是 state,useEffect 對應的就是 effect 對象,useRef 對應的就是 ref 對象

baseState

和 useState 相關,在初次渲染時等於傳入的初始 state,後續是每次計算出的新的 state

queue

相似於 fiber 對象的 updateQueue,每次調用 useState 返回的 setSomeState 方法就會建立一個 update 對象放到 queue 中,而後在 render 階段再遍歷 queue 計算出新的 state

const queue = (hook.queue = {
    last: null, // 指向最後一個 update,它的 next 指向第一個 update,這是一個循環鏈表
    dispatch: null, // dispatch 方法,用於計算出新的 state
    lastRenderedReducer: reducer, // 最後一個 update 的 reducer
    lastRenderedState: (initialState: any), // 指向最後一個 update 產生的 state
  });
複製代碼

本節解決的問題

  1. 有多個相同的 hooks 時 react 是如何區分的

Github

包含帶註釋的源碼、demos和流程圖
github.com/kwzm/learn-…

相關文章
相關標籤/搜索