梳理react-hook源碼

我只是不能忍受。。。。你一生在這裏打轉。你太聰明,太有趣。。。你只有一次生命,應該儘可能活的充實。react

前言

基於reactV17.0源碼分析數組

本片文章主要經過react的源碼,分析useState、useEffect和兩個鉤子函數的內在執行原理。緩存

  1. hook組件沒有this指針,沒有生命週期,setXxxx函數是如何發起更新,進行調度的?
  2. hook對象在react內部是以什麼結構進行儲存運算的?
  3. useEffect的第一個參數是在調度中是如何執行的,它的返回函數爲何能夠在unMount的時候觸發?
  4. useEffect第二個參數又是如何equal,避免重複執行第一個參數的函數?
  5. 等等。

爲了解決包括不限於以上的問題,咱們打開react項目研究一下源碼。限於我的技術緣由,若有問題戳我。markdown

源碼入口

領略react-hook以前,咱們先回顧一下從reactDom.render(element, dom)開始到hook節點建立中間經歷了那些步驟。下面是我根據源碼,將初次渲染的關鍵路徑簡單的梳理了一下,只涉及關鍵函數。(打開react項目,點點點)閉包

截屏2021-05-31 下午10.26.32.png

renderWithHooks

顧名思義,這個函數就是對hook組件的處理函數的入口。下面簡單的分析一下這個函數主要作了什麼:dom

  1. 將全局變量currentlyRenderFiber指向workInProgress
  2. 根據memoizedState判斷首次掛載/更新,獲取hooks方法集的Dispatcher對象,並將此對象賦值給全局變量ReactCurrentDispatchercurrent屬性
  3. 最後執行hook組件的function構造方法
// packages/react-reconciler/src/ReactFiberHooks.old.js
export function renderWithHooks() {
  
  currentlyRenderingFiber = workInProgress;
  
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  ...

  //賦值全局變量,能夠在react中使用相關hook方法
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
          ? HooksDispatcherOnMount
          
  let children = Component(props, secondArg);

  // 省略一些條件的判斷,和置空

  return children;
}

複製代碼

ReactCurrentDispatcher

  • 這個對象是全局對象,在react模塊中定義,並暴露,因此咱們能import { useState, useEffect } from 'react'
  • 並在renderWithHooks函數實現各類鉤子函數,並將各個鉤子函數掛在對應的fiber對象上的hook鏈表
  • 因此最後在執行hook的構造方法(Component())時,可以使用相關的hook方法

實現並掛載在全局變量ReactCurrentDispatcher以後,在執行hook組件的構造方法時,咱們就能拿到相關的hook方法。咱們就先拿最多見的useStateuseEffect來講明,其餘的鉤子函數先不考慮。函數

// packages/react/src/ReactCurrentDispatcher.js
//在react模塊中定義,並拋出了這個對象
const ReactCurrentDispatcher = { 
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;


//packages/react-reconciler/src/ReactFiberHooks.old.js
//在react-reconciler模塊中實現
const HooksDispatcherOnMount: Dispatcher = {
  //初次掛載
  useState: mountState,
  useEffect: mountEffect,
  
};
const HooksDispatcherOnUpdate: Dispatcher = {
  //更新
  useEffect: updateEffect,
  useState: updateState,
  
};
複製代碼

useState

首先是初次掛載階段

源碼裏咱們能夠看到,最關鍵的就是利用bind閉包,緩存了currentlyRenderingFiber對象(我也列出了源碼中對此對象的解釋,就是當前的fiber節點)和queue對象(保存的是更新對象update,在dispatchAction中咱們能夠看到)源碼分析

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
...

function mountState(initialState,){
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // 利用閉包緩存
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
複製代碼

image.png

接下來咱們能夠來看看hook對象的建立mountWorkInProgress函,在這個函數裏主要就是構造了一個hook的單項鍊表,並將workInProgressHook指針指向當前hook。對於workInProgressHook對象,源碼中也有詳細的解釋,用以保存將要加進當前fiber對象的hook鏈表優化

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;


function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製代碼

image.png

更新階段

咱們先來看一段實例代碼ui

function MyName(){
    const [name, setName] = useState()   
    return <div> <span>{name}</span> <button onClick={setName('駱家豪')}> 展現名字</button> </div>
}
複製代碼

看了上面初次掛載的解析後,咱們明白setName就是dispatchAction函數,而且利用閉包緩存了當前fiber對象的指引。so,咱們來看看最最核心的dispatchAction函數作了什麼騷操做。

function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) {
  //調用優先級相關,可看以前的文章
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);

  //建立一個更新任務
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  
  // 將更新加到鏈表的最後
  const pending = queue.pending;
  if (pending === null) {
    // 這是第一個更新,建立一個環形鏈表
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;

  ...

  //發起調度
  scheduleUpdateOnFiber(fiber, lane, eventTime);
 }
複製代碼

image.png

咱們能夠看到其實就是構造了一個update的鏈式結構,並掛在hook對象上的queue屬性上,並在最後發起一個調度。那麼調度以後是如何的從新計算fiber節點的,如何處理queue中的update更新鏈,咱們還要看updateState函數。

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

直接返回一個updateReducer函數,顧名思義,就是一個更新合併的函數。將一次或屢次setName函數產生的update合併。計算出最新的state

function updateReducer(){

  //1.合併並計算queue隊列
  let baseQueue = current.baseQueue;
  
  //2.若是存在等待的更新隊列,則循環update的單向鏈表,reducer全部的state
  if(baseQueue !== null){
    const first = baseQueue.next;
    let newState = current.baseState;
    let update = first;
    do {
        const action = update.action;
        newState = reducer(newState, action);
        update = update.next;
    } while (update !== null && update !== first)
    
  }
  
  //3.將最新的state存入當前hook,並返回
  hook.memoizedState = newState;
  hook.baseState = newBaseState;
  hook.baseQueue = newBaseQueueLast;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
複製代碼

小結

到此咱們將useState這個hook的建立,更新過程過了一遍。下面來技術總計。

  1. hook對象以鏈表的形式保存在當前fiber對象的memoizedState屬性上,造成環形結構。便於以後的便利合併。
  2. useState鉤子函數返回的第二個參數setXxx,其實就是利用閉包,對應源碼dispatchAction.bind(null, currentlyRenderingFiber,queue,)。緩存了當前的fiber對象和queue跟新隊列,這也解釋了爲何hook函數不須要this指針也能對應上指定的更新對象
  3. 在執行setXxx時,也就是建立了一個update,並將update對象以單項鍊表的形式保存在當前hookqueue屬性上。並在最後發起一個調度scheduleUpdateOnFiber,全部更新的入口函數。
  4. 執行上一個步驟的調度時,由於存在memoizedState,就會執行updateState,也就是執行了updateReducer函數,顧名思義就是合併更新
  5. 最核心的的就是updateReducer,在這個函數中將update合併,並do-while遍歷update,合併計算update對象中的action屬性,就是setXxx的第一個參數,能夠是函數。最後返回一個新的state保存在當前hook的memoizedState屬性中。

useEffect

看完useState的代碼,在看useEffect函數,大部分都是相同的。最大的不一樣仍是觸發時機的不一樣,useEffectrender過程以後觸發計算,useState在下次計算當前fiber的時候觸發執行計算newState

初次掛載與更新

咱們先一塊兒來看看 useEffect初次掛載,和更新時的代碼。

function mountEffect(create,deps) {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}
function updateEffect(create,deps) {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}
複製代碼

看了代碼其實區別就轉換成了mountEffectImplupdateEffectImpl區別。

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}
複製代碼

看了上面的掛載、更新兩個階段的函數對比,咱們很容易發現,更新階段僅僅比掛載階段多了一段判斷依賴數組是否相同的代碼,多作了一個減小重複執行的優化(hook第一個參數create)。全部在咱們平時開發中必定要注意useEffect第二個參數的使用,固然源碼中的比較函數,也只是作了一層淺比較。

hook-effect鏈表結構

那麼接下來咱們就打開pushEffect函數看看狀況。

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // 環形鏈表
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
複製代碼

其實看完很簡單,並無複雜的合併,計算。只是將生成一個effect對象,並以環形鏈表的結構存在hookmemoizedState屬性上,並將此鏈表賦值給全局變量componentUpdateQueueimage.png

處理effect鏈表

最中造成一個effect的環形鏈表後,放在了componentUpdateQueue中。至於什麼時候觸發,找了一會,在commitHookEffectListMount函數中找到了對hook-effect的處理

image.png 下面咱們就詳細的看下commitHookEffectListMount如何處理effect鏈表

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 作了判斷,過濾了依賴重複時,push進來的effect
      if ((effect.tag & tag) === tag) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
複製代碼

其實很簡單,就是遍歷effect鏈表,並利用tag過濾依賴dept沒變產生的effect。執行create函數,而後將create函數執行的結果賦值給destory屬性。

而後繼續在突變階段-commitWork函數找到了destory執行

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
複製代碼

總結

  1. useState,仍是useEffect都是產生一個鏈表掛載在當前hook對象的memoizedState屬性上。
  2. 不一樣的事useState產生的是update更新對象的鏈表,useEffect產生的是effect反作用鏈表
  3. 這兩個鉤子函數觸發時機不一樣。useState在下次更新時,合併計算hook對象上的update鏈表,最中計算出最新的newState,賦值給hook.momoizedState;而useEffect則在commit階段的突變後纔開始執行hook上的effect鏈表的create函數,在commit階段的突變時執行destory函數
相關文章
相關標籤/搜索