[React 源码] React 18.2 - 高优先级打断低优先级的更新 [1.5k 字 - 阅读时长3.5min]

3,328 阅读5分钟

打断低优先级任务效果展示:

delete.gif

function FunctionComponent() {
  const [number, setNumber] = React.useState("1");
  React.useEffect(() => {
      setNumber((pre) => (pre += "3"));
  }, []);
  return (
    <button
       onClick={() => {
        setNumber((pre) => (pre += "2"));
      }}
    >
      {number}
    </button>
  );
}


const element = <FunctionComponent />;
const container = document.getElementById("root");
const root = createRoot(container);
root.render(element);

juejin.cn/post/718509… , 初次渲染我们已经在这篇文章当中聊过了。

打断低优先级任务原理

第一:挂载之后,执行 useEffect 中的副作用,setNumber((pre) => (pre += "3"))后, React 从根节点开始调度更新。正准备在浏览器的空闲时间里,去继续调度更新 FunctionComponent fiber 节点之前,触发了点击事件,所以浏览器的下一帧会先触发点击事件的回调函数,然后再继续调度更新。

浏览器执行 React 代码的时机,是在一帧最后的空闲时间。所以 event 事件会先执行。

image.png

第二:event 回调函数中调用 setNumber(setNumber((pre) => (pre += "2))) , 事实上触发了 dispatchSetState 函数 又调用 scheduleUpdateOnFiber 从 根节点开始执行。

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
      const eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);

  markUpdateInDevTools(fiber, lane, action);
}

第三: scheduleUpdateOnFiber 函数中调用 ensureRootIsScheduled 函数,就是在这个函数中实现了打断。打断原理实现如下,existingCallbackNode 就是上一个更新 调度的 performConcurrentWorkOnRoot 函数,Scheduler cancelCallback 函数拿到 existingCallbackNodetask.callback 置为 null。所以当再此调度的时候,发现第一次 task 的 callback 属性已经为 null 了,直接将第一次更新的 task 弹出优先级队列。至此第一次更新被打断。

  if (existingCallbackNode != null) {
   // Cancel the existing callback. We'll schedule a new one below.
   cancelCallback(existingCallbackNode);
 }

第四:打断上一次更新之后,紧随其后的就是下一次更新,由于点击事件 的 lane 是同步优先级 1,所以 includesSyncLane 成立,包含同步优先级,所以以同步优先级调用 performSyncWorkOnRoot -> renderRootSync -> workLoopSync

  // Schedule a new callback.
  let newCallbackNode;
  if (includesSyncLane(newCallbackPriority)) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue
    if (root.tag === LegacyRoot) {
      if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
        ReactCurrentActQueue.didScheduleLegacyUpdate = true;
      }
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

第五:假定又遍历到了该函数节点,又调用了 useState, 事实上是 调用了 updateReducer, 遍历更新队列,由于是以同步优先级调用的。所以第一个 update 会被跳过,直接执行第二个 update 对象,计算出新状态 -> 提交,至此,UI 渲染出了最高优先级的任务。

if (shouldSkipUpdate) {
  const clone = {
    lane: updateLane,
    action: update.action,
    hasEagerState: update.hasEagerState,
    eagerState: update.eagerState,
    next: null,
  };
  if (newBaseQueueLast === null) {
    newBaseQueueFirst = newBaseQueueLast = clone;
    newBaseState = newState;
  } else {
    newBaseQueueLast = newBaseQueueLast.next = clone;
  }
  currentlyRenderingFiber.lanes = mergeLanes(
    currentlyRenderingFiber.lanes,
    updateLane
  );
}

打断低优先级任务


但是仍然没有结束,被打断了的低优先级任务,将高优先级任务提交之后,还需要再 commitRootImpl函数,调用 ensureRootIsScheduled 函数执行。

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority
) {
  // Always call this before exiting `commitRoot`, to ensure that any
  // additional work on this root is scheduled.
  ensureRootIsScheduled(root, now());
  return null;
}

第一:虽然低优先级更新的调度任务取消了,但是该 setNumebr 产生的两个更新对象还在按照交互顺序排列在 hook.queue.pending 当中,由于低优先级的任务被跳过了,所以 baseQueue 还是 +=3 -> += 2 ,baseState 还是 1, 所以最后的结果是 132。

如果不打断,2s 之后去点击按钮, 结果就是动画当中的 132。

问题来了?那什么时候,会出现 123 呢,就是在 2s 之前只要点击了按钮,就会出现 123的结果。

最后的队列执行结果,大家可能有些模糊,可以带着目的去读源码,从目的推到过程,目的是 React 在保证优先级高的任务打断低优先级的任务先执行的同时,也要保证最后的执行结果是正确的。

  /* 
   queue:  1 -> 2 -> 3
   1 执行, 2 被跳过 baseQueue 就是 2 -> 3, baseState: 1
   1 被跳过, 2, 3执行 baseQueue 就是 1 -> 2 -> 3  baseState: 初始值
   */
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;
  const current: Hook = (currentHook: any);
  
  /*  1-> 2 -> 3
   1 执行, 2 被跳过 baseQueue 就是 2 -> 3
   1 被跳过, 2, 3执行 baseQueue 就是 1 -> 2 -> 3
   */
   
  // baseQueue 是上一次被跳过更新的队列 
  let baseQueue = current.baseQueue;

  // The last pending update that hasn't been processed yet.
  const pendingQueue = queue.pending;
   // 如果有待生效的队列
  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    // 将上一次被跳过的更新的队列进行合并
    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;
  }
  // 如果有更新
  if (baseQueue !== null) {
    // We have a queue to process.
    // 基本状态
    let newState = queue.baseState;
    // 新的车道
    let newLanes = NoLanes;
    // 新的基本状态
    let newBaseState = null;
    // 新的第一个基本更新
    let newFirstBaseUpdate = null;
    // 新的最后一个基本更新
    let newLastBaseUpdate = null;
    // 第一个更新
    let update = firstBaseUpdate;
    do {
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
      // 判断优先级是否足够,如果不够就跳过此更新
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        // This update does have sufficient priority.
         // 如果已经有跳过的更新了,即使优先级再高也需要添到新的基本链表中
        if (newBaseQueueLast !== null) {
          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.
        const action = update.action;
        if (shouldDoubleInvokeUserFnsInHooksDEV) {
          reducer(newState, action);
        }
        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 {
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

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


    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }