banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useState 分析

說明#

  1. 本文基於 v18.1.0 進行分析。
  2. 在對於 useState 進行調試時會反覆出現 interleaved 這一概念,請參考該 鏈接
  3. 文章大致上基於下面的代碼進行分析,根據情況不同會有一定的改動。
  4. 2022-10-14 補充了關於批處理的源碼分析(涉及 任務重用【相關】 與 高優先級插隊【代碼裡有順嘴一提】)與調度階段的簡單總結
import { useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => { setText(1) } }>{ text }</div>
  )
}

export default UseState;

TLNR#

  1. useState 的 dispatch 是一個異步操作
  2. useState 的實現基於 useReducer,或者說 useState 就是一個特殊的 useReducer
  3. useState 每次調用其 dispatch 都會生成一個對應的 update 實例,多次調用同一個 dispatch 則會掛載成為一個 update 的鏈表。並且每個 update 都擁有一個對應的優先級(lane)
  4. 如果生成的 update 被判斷為需要被調度更新,則會以 microTask 的形式被調度更新
  5. 在 useState 的 update 階段內為了快速響應高優先級的 update,update 鏈表只會先處理高優先級的 update,低優先級的 update 會在下一次 render 階段內被調用更新。
  6. 批處理與(調度 → 協調 → 渲染)內的調度階段總結請移步文末

mount 場景下的 useState#

直接對 useState 打斷點進行調試,我們會進入 mountState 內

useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  ···
    // initialState 為 useState 傳入的初始化值
    return mountState(initialState);
  ···
}

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // hooks 鏈表生成步驟,見[之前的文章](https://github.com/IWSR/react-code-debug/issues/2)
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // initialState 的類型聲明表示它也可以是個函數
    initialState = initialState();
  }
  // hook 上的 memoizedState、baseState 會緩存 useState 的初始值
  hook.memoizedState = hook.baseState = initialState;
  // 更新時會提到,比較重要的結構
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue; // useState 對應的 hooks 對象會多個 queue 屬性
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any)); // 本文將分析的重點,非常重要的函數
  // 對應 const [text, setText] = useState(0); 的返回值
  return [hook.memoizedState, dispatch];
}

就這樣 mount 階段的 useState 的分析完了,東西很少。

另外請注意一下 basicStateReducer 這一函數。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

總結#

mount 階段的 useState 大致起到一個初始化的作用,在結束該函數的調用後我們會獲得該 useState 對應的 hooks 對象。該對象長這樣

image

觸發 useState 的 dispatch#

此時將斷點打到 useState 返回的 dispatch 上,觸發點擊事件。獲得下面的調用棧

image

dispatchSetState 是個關鍵的函數,再次強調一下。

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {

  /**
   * 這裡暫時理解為獲取當前 update 的優先級即可
  */
  const lane = requestUpdateLane(fiber); 

  /**
   * 與 setState 的 update 鏈表類似
   * 這裡的 update 也是一個鏈表結構
  */
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  /**
   * 判斷是否是渲染中更新
  */
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    /**
     * 判斷 react 當前是否空閒
     * 比方說在 第一次調用 dispatch 後,fiber.lanes 就不為 NoLanes
     * 因此 第二次調用 dispatch 便不會進入底下對 update 實例內的 eagerState 求值
    */
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;

        const currentState: S = (queue.lastRenderedState: any);
        /**
         * 計算期望的 state 並暫存入 eagerState
         * lastRenderedReducer 內邏輯如下
         * 判斷當前 action 是否是一個函數
         * 畢竟也存在 setText((preState) => preState + 1)這樣的調用方式存在
         * 如果是函數傳入 currentState 並調用 action 獲得計算後的值
         * 如果不是,比如setText(1)這樣的調用方式,就直接返回 action
        */
        const eagerState = lastRenderedReducer(currentState, action);
        // Stash the eagerly computed state, and the reducer used to compute
        // it, on the update object. If the reducer hasn't changed by the
        // time we enter the render phase, then the eager state can be used
        // without calling the reducer again.
        update.hasEagerState = true; // 根據註釋 防止被 reducer 重複計算
        update.eagerState = eagerState;
        /**
         * 判斷兩者的值是否相等 淺比較 ===
        */
        if (is(eagerState, currentState)) {
          // Fast path. We can bail out without scheduling React to re-render.
          // It's still possible that we'll need to rebase this update later,
          // if the component re-renders for a different reason and by that
          // time the reducer has changed.
          // TODO: Do we still need to entangle transitions in this case?
          /**
           * 根據註釋,如果值未改變,則不需要去調度 react 去重新渲染
           * 底下這個函數只是去處理 update 鏈表的結構
          */
          enqueueConcurrentHookUpdateAndEagerlyBailout(
            fiber,
            queue,
            update,
            lane,
          );
          return; // 因為不需要調度,因此函數在這裡中斷
        }
        
      }
    }
    /**
     * 因為運行到這裡的 update 是被認為必須要被重新渲染到頁面上的
     * enqueueConcurrentHookUpdate 與 enqueueConcurrentHookUpdateAndEagerlyBailout 不同
     * 會去調用 markUpdateLaneFromFiberToRoot
     * 該函數會將當前 update 對應的 lane 自發生更新的 fiber 節點開始一層一層向上遞歸(fiber.return)
     * 標記到其父 fiber 的 childLanes 上(比方說fiber.return -> fiber.return.return),直到 root 為止
     * 調用 markUpdateLaneFromFiberToRoot 的目的是為 scheduleUpdateOnFiber 的調度做準備的
    */
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      /**
       * 進入調度流程(scheduleUpdateOnFiber 的分析已經更新在最下面了)
       * 不過在 v18 中會被 queueMicrotask 註冊為 microTask
       * 在 v17 則是以對應的優先級註冊進 scheduler
      */
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      /**
       * 一個不穩定的函數,大致上是處理 lanes,有點難懂不解析了
      */
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane, action);
}

總結一下 useState 的 dispatch#

對於 useState 的 dispatch 來說,每調用一次 dispatch 都會生成一個新的 update 實例且該實例會被掛載到其 hooks 的鏈表,但是調用 dispatch 後生成的新值會與其原始值(currentState)經過一次淺比較來決定這一次的更新是否需要被調度更新。在 v18 的版本中,這一次的調度任務是存在於 microTask 中的,因此可認為 useState 的 dispatch 屬於異步操作。下面簡單寫個例子驗證下。

import { useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => { setText(1); console.log(text); } }>{ text }</div>
  )
}

export default UseState;

點擊後得到的結果

image

其調用棧,不難看到 microtask。

image

update 場景下的 useState#

接下來我們稍微在原來的調試代碼上添加一個 dispatch,新的調試代碼大概長這樣。

import { useEffect, useState } from "react";

function UseState() {
  const [text, setText] = useState(0);

  return (
    <div onClick={ () => {
      setText(1);
      setText(2); // 給這裡打個斷點
    } }>{ text }</div>
  )
}

export default UseState;

對整個頁面的表現做一個記錄,大致長這樣

image

順帶一提

第二次 dispatch 時因為會不滿足 fiber.lanes === NoLanes 這個條件因此直接跳入 enqueueConcurrentHookUpdate (具體看一下 dispatchSetState 的代碼)

而在 enqueueConcurrentHookUpdate 對 update 鏈表連接後我們會得到如下結構的環状鏈表

image

回到正題,在 useState 的 update 階段中會調用 updateState 內的 updateReducer 這一函數(就是 uesReducer 對應的 mount 版本)

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  /**
   * 這個函數在 use(Layout)Effect 內也出現過,大致作用是
   * 同時移動 WIP hooks 和 current hooks 的指針向後移動一位
   * 詳細註釋可看 [React Hooks: 附錄](https://github.com/IWSR/react-code-debug/issues/4)
  */
  const hook = updateWorkInProgressHook(); // WIP hooks對象
  const queue = hook.queue; // update 鏈表

  if (queue === null) {
    throw new Error(
      'Should have a queue. This is likely a bug in React. Please file an issue.',
    );
  }

  // 在 useState 的場景中 lastRenderedReducer 為 basicStateReducer —— 在 mountState 中預設的 reducer
  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // The last rebase update that is NOT part of the base state.
  let baseQueue = current.baseQueue;

  // The last pending update that hasn't been processed yet.
  // 獲取 pending 狀態的 update,還未被計算
  const pendingQueue = queue.pending;
  /**
   * 整理 update queue,為後續的更新做準備
  */
  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    // 將 pendingQueue 與 baseQueue 相連
    if (baseQueue !== null) {
      // Merge the pending queue and the base queue.
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  /**
   * 處理後的 baseQueue 包含了所有 update 實例
  */
  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = update.lane;
      /**
       * 篩選出被包含在 renderLanes 內的 update
       * 這步驟和位操作有關
       * updateLane 如果屬於 renderLanes 內,則代表著當前 update 的優先級比較緊急
       * 需要被處理,反之則可以跳過,
       * 類似的邏輯也存在於 class 組件的 update 鏈表內(這裡不詳細展開)
       * 不過這裡是 ! 操作,所以都是反的
      */
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        /**
         * 進入了這個邏輯的 update 便意味著不是很緊急,可以慢慢處理
         * 但是這並不意味著不處理,在空閒時仍然會從被跳過的 update 開始重新再計算一次狀態
         * 因此需要緩存被跳過的 update 實例
        */
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        /**
         * 將跳過的 update 添加到 newBaseQueue 中
         * 等到下一次 render 再重新計算
        */
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // Update the remaining priority in the queue.
        // TODO: Don't need to accumulate this. Instead, we can remove
        // renderLanes from the original lanes.
        // 更新 WIP fiber 的 lanes,下一次 render 時重新執行跳過 update 的關鍵
        // completeWork 時會將這些 lanes 收集到 root(合併到 root 發生在 commitRootImpl),然後再重新調度(commitRootImpl 內的 ensureRootIsScheduled)
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        // 標記跳過的 lanes 到 workInProgressRootSkippedLanes 上
        markSkippedUpdateLanes(updateLane);
      } else {
        // This update does have sufficient priority.
        // 優先級足夠的場景,便執行更新

        if (newBaseQueueLast !== null) {
          /**
           * newBaseQueueLast 不為空,說明存在被跳過的 update
           * update 的狀態計算可能存在聯繫
           * 因此一旦有update被跳過,就以它為起點,
           * 將後邊直到最後的 update 無論優先級如何都截取下來。
           * (和 class 組件的處理邏輯差不多)
          */
          const clone: Update<S, A> = {
            // This update is going to be committed so we never want uncommit
            // it. Using NoLane works because 0 is a subset of all bitmasks, so
            // this will never be skipped by the check above.
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // Process this update.
        // 執行本次的 update,計算新的 state
        if (update.hasEagerState) {
          // 已經計算過的狀態會被打上標記,避免重複計算
          // If this update is a state update (not a reducer) and was processed eagerly,
          // we can use the eagerly computed state
          newState = ((update.eagerState: any): S);
        } else {
          // 根據 state 和 action 計算新的 state
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    // === 校驗不通過,則在當前的 WIP fiber 上標記更新,在 completeWork 階段中會被收集到 root 上
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    // 更新新的數據到 hook 內
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  // Interleaved updates are stored on a separate queue. We aren't going to
  // process them during this render, but we do need to track which lanes
  // are remaining.
  /**
   * 處理 interleaved update
   * 但我決定跳過這一塊的分析,因為我完全不知道什麼樣的更新才能被叫做 interleaved update
   * 在 /react-reconciler/src/ReactFiberWorkLoop.old.js 內有著這一塊的描述
   * Received an update to a tree that's in the middle of rendering. Mark
that there was an interleaved update work on this root.
   * 但是又和 enqueueConcurrentHookUpdate 與 
   * enqueueConcurrentHookUpdateAndEagerlyBailout 內對於 queue.interleaved 的設置完全搭不上邊
   * 如果有對這方面有了解的朋友也請麻煩指導一下
  */
  const lastInterleaved = queue.interleaved;
  if (lastInterleaved !== null) {
    let interleaved = lastInterleaved;
    do {
      const interleavedLane = interleaved.lane;
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        interleavedLane,
      );
      markSkippedUpdateLanes(interleavedLane);
      interleaved = ((interleaved: any).next: Update<S, A>);
    } while (interleaved !== lastInterleaved);
  } else if (baseQueue === null) {
    // `queue.lanes` is used for entangling transitions. We can set it back to
    // zero once the queue is empty.
    queue.lanes = NoLanes;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

總結一下#

useState 的 update 階段會通過 dispatch 函數生成的 update 鏈表去更新對應的狀態,然而並非所有的 update 都會被更新,只有其優先級(lane)屬於當前 renderLanes 內才會被優先計算,而跳過的 update 會被標記並在下一次 render 時被更新。

簡單提供個例子來證明

import { useEffect, useState, useRef, useCallback, useTransition } from "react";

function UseState() {
  const dom = useRef(null);
  const [number, setNumber] = useState(0);
  const [, startTransition] = useTransition();

  useEffect(() => {
    const timeout1 = setTimeout(() => {
      startTransition(() => { // 將其優先級降級,從而被 timeout2 內的 update 插隊
        setNumber((preNumber) => preNumber + 1);
      });
    }, 500 )

    const timeout2 = setTimeout(() => {
      dom.current.click();
    }, 505)

    return () => {
      clearTimeout(timeout1);
      clearTimeout(timeout2);
    }
  }, []);

  const clickHandle = useCallback(() => {
    console.log('click');
    setNumber(preNumber => preNumber + 2);
  }, []);

  return (
    <div ref={dom} onClick={ clickHandle }>
      {
        Array.from(new Array(20000)).map((item, index) => <span key={index}>{ number }</span>)
      }
    </div>
  )
}

export default UseState;

從運行結果來看,由於 timeout1 的 update 被 startTransition 降級,因此 timeout2 的 update 被優先更新了。

Sep-07-2022 16-57-09

批處理#

批處理其實在 v17 時代便已經有了,不過 v18 把它的限制條件給去除了然後就成了自動批處理(惱)。具體可以看看這篇文章

比方說我們這麼一段代碼在運行(當然這裡的調試環境依然是 v18)

import { useEffect, useState } from "react";

function UseState() {
  const [num1, setNum1] = useState(1);
  const [num2, setNum2] = useState(2);

  useEffect(() => {
    setNum1(11);
    setNum2(22);
  }, []);

  return (
    <>
      <div>{ num1 }</div>
      <div>{ num2 }</div>
    </>
  )
}

根據上文中對 dispatchSetState 的分析中其實有提到,只要是有必要更新到頁面的 update 就會調用 scheduleUpdateOnFiber 來觸發渲染。那么例子中連續調用兩次 dispatch ,也就是說觸發了兩次 scheduleUpdateOnFiber,那麼問題來了 —— 頁面渲染了幾次呢?

image

就一次。

至於為什麼需要去看 scheduleUpdateOnFiber 裡面做了什麼,scheduleUpdateOnFiber 涉及到了整個更新的入口,因此也是一個比較重要的函數。

該函數主要負責處理

  1. 檢查是否存在無限更新 —— checkForNestedUpdates
  2. 在 root.pendingLanes 上標記存在需要更新的 update 的 lanes —— markRootUpdated
  3. 觸發 ensureRootIsScheduled,進入任務調度的核心函數

scheduleUpdateOnFiber#

  export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  Count the number of times the root synchronously re-renders without
  // finishing. If there are too many, it indicates an infinite update loop.
  // 檢查是否存在無限更新,主要通過設置了一個上限 —— NESTED_UPDATE_LIMIT(50)
  checkForNestedUpdates();

  // Mark that the root has a pending update.
  // 在 root.pendingLanes 上標記存在需要更新的 update 的 lanes
  markRootUpdated(root, lane, eventTime);

  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    /**
     * 錯誤處理,跳過
    */
  } else {
    ...
    /**
     * 重要函數!觸發任務調度的核心
     * 我們要找的邏輯也在裡面
    */
    ensureRootIsScheduled(root, eventTime);
    ...
    // 底下是 Legacy 模式的兼容代碼,不看了
  }
}

這時我們又遇到了一个 react 內的重要函數 —— ensureRootIsScheduled,進入該函數就能看到任務調度在 react 內的核心邏輯了(還有一部分核心邏輯在 Scheduler 內,可以看我寫的 React Scheduler: Scheduler 源碼分析

ensureRootIsScheduled#

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  /**
   * root.callbackNode 是 Scheduler 在執行調度時生成的任務,
   * 而這個值會在 react 調用 scheduleCallback 時給 return 出來並賦值給 root.callbackNode
   * 而從 existingCallbackNode 的變量名上不難推測,這個變量用以表示已經存在的被調度的任務(舊任務)
  */
  const existingCallbackNode = root.callbackNode;

  // Check if any lanes are being starved by other work. If so, mark them as
  // expired so we know to work on those next.
  /**
   * 這裡是為了檢查正在等待的任務中是否存在已經過期了的任務(比方說一些高優先級的任務一直插隊,
   * 導致低優先級的任務沒法被執行而過期了),如果存在過期了的任務,則把他們的 lane標記到 
   * expiredLanes 上,方便 react 以同步優先級立刻調用他們
  */
  markStarvedLanesAsExpired(root, currentTime);

  // Determine the next lanes to work on, and their priority.
  // 獲取 renderLanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  /**
   * 如果 renderLanes 為空,那麼意味當前不需要啟動調度,跳出
  */
  if (nextLanes === NoLanes) {
    // Special case: There's nothing to work on.
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // We use the highest priority lane to represent the priority of the callback.
  /**
   * 生成 renderLanes 內最優先的任務對應的 優先級
  */
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // Check if there's an existing task. We may be able to reuse it.
  /**
   * 獲取舊任務的優先級
  */
  const existingCallbackPriority = root.callbackPriority;
  /**
   * 如果兩個任務的優先級相同,那麼直接跳出,因為已經存在了對應的任務可以復用
   * 這裡也是實現 批處理 的地方
  */
  if (
    existingCallbackPriority === newCallbackPriority
  ) {
    // The priority hasn't changed. We can reuse the existing task. Exit.
    // 優先級未改變,因此可以復用舊任務,於是退出
    return;
  }

  /**
   * 插隊邏輯
   * 進入這裡的邏輯都可以認為其優先級比舊任務的優先級要高,於是需要重新調度
   * 於是舊任務就沒有被調用的必要了,於是可以取消了。(取消邏輯可以看 Scheduler 內的邏輯)
  */
  if (existingCallbackNode != null) {
    // Cancel the existing callback. We'll schedule a new one below.
    cancelCallback(existingCallbackNode);
  }

  // 調度一個新任務
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue
    /**
     * 同步優先級意味著這可能是過期任務或是當前的模式為非 concurrent 模式
    */
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    if (supportsMicrotasks) {
      // Flush the queue in a microtask.
      /**
       * 要是瀏覽器支持 microtask,就把 performSyncWorkOnRoot 丟到 microtask 內
       * 這樣子就可以更快的執行
       * 一個 eventLoop 遵循 一個old MacroTask ——> 清空microtasks ——> 瀏覽器渲染頁面 這樣的順序執行
       * 不然進入 Scheduler 內,MessageChannel 處理的任務可都是 MacroTask,那就會在下個
       * eventLoop 內再渲染了
      */
      scheduleMicrotask(() => {
        if (
          (executionContext & (RenderContext | CommitContext)) ===
          NoContext
        ) {
          /**
           * 這裡會把上面調度的任務取消掉然後再調用 performSyncWorkOnRoot
          */
          flushSyncCallbacks();
        }
      });
    } else {
      // Flush the queue in an Immediate task.
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    // concurrent模式的處理邏輯
    let schedulerPriorityLevel;
    // 把 renderLanes 轉成 調度優先級
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
    /**
     * 用對應的優先級讓 Scheduler 調度 react的任務了
    */
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  // 更新root上的任務優先級和任務,以便下次發起調度時可以獲取到
  // 就開頭用到的 oldTask
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

分析完 ensureRootIsScheduled 我們也對批處理是如何實現的有了大致的了解,它的實現其實就是對 react 內同優先級任務的復用,這樣子就做到了多次觸發 hooks 的 dispatch 但是只渲染了一次。

不過到這配合上 React Scheduler: Scheduler 源碼分析 其實我們已經把 react 內三大模塊(調度 → 協調 → 渲染),調度的部分全部介紹完了,接下來讓我一個問題來把整篇文章串起來。

React 從觸發 useState 的 dispatch 到渲染到頁面做了哪些事 —— 調度篇

  1. 生成一個對應優先級的 update 實例,並以循環鏈表的形式掛載到 useState 對應的 hooks 實例上的 queue 屬性上
  2. 如果當前 update 被判斷為需要被渲染到頁面上的,則會將這個 update 上的 lane 自當前 fiber 節點向其父節點的 childLanes 上層層標記,直到 root 為止
  3. 隨後會調用 scheduleUpdateOnFiber 進入調度階段
    1. 首先將當前 update 的 lanes 標記到 root.pendingLanes 上,pendingLanes 上的任務可以視為所有待更新的任務
    2. 隨後取 pendingLanes 內最高的優先級作為當前的 renderLanes
    3. 在使用 Scheduler 註冊調度任務前,會先檢查是否存在已註冊但還未執行的任務(root.callbackNode),如果存在任務便用該任務的任務優先級與 renderLanes 比較
      1. 任務優先級相同,則復用該任務;
      2. 如果 renderLanes 更高則將舊任務(root.callbackNode)取消重新在 Scheduler 內註冊任務,並將返回的任務賦值給 root.callbackNode
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。