IWSR / react-code-debug

react v18 源码分析
7 stars 1 forks source link

React Hooks: useState #3

Open IWSR opened 2 years ago

IWSR commented 2 years ago

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 来说,每调用一次 dispathc 都会生成一个新的 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 在执行调度时生成的 task,
   * 而这个值会在 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 内注册任务,并将返回的 task 赋值给 root.callbackNode
IWSR commented 2 years ago
TyroneYvesChen commented 2 years ago

M 大佬太强了!

IWSR commented 2 years ago

useRe

  • [x] v18关于批处理的内容
  • [ ] 加入 useReducer

useReducer 不在这里分析了,这篇文章涉及的东西太多了,决定把 useReducer 作为独立章节。

IWSR commented 2 years ago

自动批处理作为 v18 的新特性,只有『自动』两个字能叫新