React(v16.8) Hooks 簡析

動機

  • 在組件之間複用狀態邏輯很難, providers,consumers,高階組件,render props等能夠將橫切關注點 (如校驗,日誌,異常等) 與核心業務邏輯分離開,可是使用過程當中也會帶來擴展性限制,ref傳值問題,「嵌套地獄」等問題;Hook 提供一種簡單直接的代碼複用方式,可使開發者在無需修改組件結構的狀況下複用狀態邏輯。html

  • 複雜組件生命週期經常包含一些不相關的邏輯,相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒;不少開發者將 React 與狀態管理庫結合使用,這每每會引入了不少抽象概念,開發過程當中還須要在不一樣的文件之間來回切換;Hook 提供一種更合理的代碼組織方式,能夠將組件中相互關聯的代碼彙集在一塊兒,而不是被生命週期方法強制拆開,使其更加可預測。前端

  • class 組件存在着一些問題(如:class 不利於代碼壓縮,而且會使熱重載出現不穩定的狀況);Hook支持函數組件,使開發者在非class 的狀況下可使用更多的 React 特性。react

使用

Hooks 只能在函數組件 (FunctionComponent) 中使用,賦予無實例無生命週期的函數組件以 class 組件的表達力而且更合理地拆分/組織代碼,解決複用問題。git

實現原理

Fiber 提供了 hooks 實現的基礎:hooks 是基於 Fiber 對象上能存儲 memoizedState,它以雙向鏈表的形式存儲在 Fiber 的 memoizedState 字段中。github

什麼是 Fiber?

Fiber 把應用樹區分紅每個節點的更新,每個 ReactElement 對應一個 Fiber 對象,Fiber 呈鏈表結構,串聯整個應用樹結構 (child, siblings, return),如圖:數組

Fiber Tree

Fiber 會記錄節點的各類狀態 (state, props)(包括functional Component),而且在 update 的時候,會從原來的 Fiber(current)clone 出一個新的 Fiber(alternate)。兩個 Fiber diff 出的變化(side effect)記錄在 alternate上,在更新結束後 alternate 會取代以前的 current 的成爲新的 current 節點。瀏覽器

Fiber 對 react 渲染機制的改變主要的影響:微信

  • 異步更新:由於 Fiber 把應用樹區來分紅每個節點的更新,它們的更新互相獨立,不會有相互的影響,因此能夠異步打斷如今的更新,而後去等待一個別的任務執行完成以後回過頭來繼續進行更新。數據結構

  • 提供了 hooks 實現的基礎:hooks 是基於 Fiber 對象上能存儲 memoizedState, 基於 memoizedState 上能夠存儲這些東西,一步一步向下構建了 hooks API 的體系。dom

主要 Hooks

  • 經常使用的:useState, useEffect, useContext, useReducer;

  • 此外不經常使用的:useLayoutEffect, useCallback, useMemo, useRef, useImperativeHandle。

以 useState 爲例瞭解 hook 的渲染更新過程 先了解 Hook 的數據結構

export type Hook = {
  memoizedState: any, //上一次渲染的時候的state

  baseState: any, // 當前正在處理的state
  baseUpdate: Update<any, any> | null, // 當前的更新
  queue: UpdateQueue<any, any> | null, // 產生的update放在這個隊列裏

  next: Hook | null, // 下一個
};

複製代碼

運行下面的組件代碼

export default function App() {
  const [name, setName] = useState('dora')

  const nameChange = e => {
    setName(e.target.value)
  }

  return (
    <React.Fragment>
      <input type='text' value={name} onChange={nameChange} />
      <p>{name}</p>
    </React.Fragment>
  )
}

複製代碼

初始化 state 時調用 mountState,初始化 initialState,而且記錄在 workInProgressHook.memoizedState 和 workInProgressHook.baseState上,而後建立 queue 對象, queue 的 dispatch 屬性是用來記錄更新 state 的方法的,dispatch 就是 dispatchAction綁定了對應的 Fiber 和 queue。而後返回初始的格式 [name, setName] = useState('dora');執行源碼以下:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
複製代碼

再來看更新過程

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

複製代碼

因而可知 useState 只是個語法糖,本質就是 useReducer;那麼再來看 useReducer:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  if (numberOfReRenders > 0) {
    //在當前更新週期中又產生了新的更新
    //就繼續執行這些更新直到當前渲染週期中沒有更新爲止
    ...
  }
  const last = queue.last;
  const baseUpdate = hook.baseUpdate;
  const baseState = hook.baseState;

  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }
if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        if (!didSkip) {
          didSkip = true;
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        if (updateExpirationTime > remainingExpirationTime) {
          remainingExpirationTime = updateExpirationTime;
        }
      } else {
        markRenderEventTimeAndConfig(
          updateExpirationTime,
          update.suspenseConfig,
        );
        if (update.eagerReducer === reducer) {
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);
    if (!didSkip) {
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

複製代碼

就是根據 reducer 和 update.action 來建立新的 state,並賦值給 Hook.memoizedState以及 Hook.baseState;在當前更新週期中又產生了新的更新, 就繼續執行這些更新直到當前渲染週期中沒有更新爲止。而後對每一個更新判斷其優先級(根據expirationTime值的大小),若是不是當前總體更新優先級內得更新會跳過,第一個跳過得 Update 會變成新的 baseUpdate,他記錄了在以後全部得 Update,即使是優先級比他高得,由於在他被執行得時候,須要保證後續的更新要在他更新以後的基礎上再次執行。

最後執行 dispatchAction方法,發起一次 scheduleWork 的調度,完成更新,此處省略代碼。

useEffect vs useLayoutEffect

useEffect 和 useLayoutEffect 帶給 FunctionalComponent 產生反作用能力的 Hooks,他們的行爲很是相似 componentDidMount 和 componentDidUpdate 的合集,而且經過 return 一個函數指定如何「清除」反作用。

先看 useEffect 和 useLayoutEffect 更新的過程:

updateEffect

updateLayoutEffect

二者都調用了 updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) 方法,傳入的第二個參數又做爲 pushEffect 的入參生成一個新的 effect。

updateEffectImpl

這個 effect 的 tag 就是入參 hookEffectTag,pushEffect 方法返回一個新的 effect,而且建立了一個 updateQueue,這個 queue 會在 commit 階段被執行。

能夠看到,這個階段,useEffect 和 useLayoutEffect 的主要區別在生成的 effect 的 tag參數不一樣,經過計算,二者的tag值分別爲二進制:0b11000000 和 0b00100100;

再來看 commit 階段調用的 commitHookEffectList 方法。

commitHookEffectList

經過對比傳入的 effectTag(unmountTag & mountTag) 和 Hook 對象上的 effectTag,判斷是否須要執行對應的 destory 和 create 方法,那麼又在哪些地方調用了commitHookEffectList 方法呢?能夠看下其中兩處:commitLifeCycles 和 commitWork。

commitLifeCycles

commitWork

在 commitLifeCycles 中傳入的 unmountTag 和 mountTag 值分別爲:0b00010000 和 0b00100000; 在 commitWork 中傳入的 unmountTag 和 mountTag 值分別爲:0b00000100 和 0b00001000; 分別計算 effect.tag & unmountTag 和 effect.tag & mountTag:

conclusion

能夠看到 useLayoutEffect 的 destory 會在 commitWork 的時候被執行;而他的 create會在 commitLifeCycles 的時候被執行;useEffect 在這個流程中都不會被執行。

事實上:

  • useLayoutEffect 會在當前 commit 執行的過程當中就會被執行 destroy 和 create, 而對於 useEffect,會異步地等到此次全部的 dom 節點更新完成,瀏覽器渲染完成後,纔會去執行這部分代碼。

  • 它對於 useLayoutEffect 來講,它是不會去阻塞瀏覽器的渲染,由於咱們可能在 useLayoutEffect 裏面去執行一些 dom 相關的操做,甚至 setState 來執行一些更新,這種更新都會同步執行,至關於 react 的運行時它要佔用更長的 js 的運行時間,致使瀏覽器沒有時間去渲染,最終可能會致使頁面會有些卡頓。

  • 服務端渲染狀況下,不管 useLayoutEffect 仍是 useEffect 都沒法在 Javascript 代碼加載完成以前執行。能夠經過使用 showChild &&進行條件渲染,並使用 useEffect(() => { setShowChild(true); }, []) 延遲展現組件。

  • useLayoutEffect 的執行過程跟 componentDidMount 和 componentDidUpdate 很是類似,因此 React 官方也說了,若是你必定要選擇一個相似於生命週期方法的 Hook,那麼 useLayoutEffect 是不會錯的那個,可是咱們推薦你使用 useEffect,在你清楚他們的區別的前提下,後者是更好的選擇。

更多 Hook 使用,請查看如下文檔

官方文檔 zh-hans.reactjs.org/docs/hooks-…

官方源碼 github.com/facebook/re…

做者:朵拉

掃碼關注:「銅板街科技」微信公衆號,精彩內容按期推送。公衆號消息界面回覆「推薦」「前端」「Java」「客戶端」獲取更多精準內容。

相關文章
相關標籤/搜索