likes
comments
collection
share

React源码解读之任务调度流程

作者站长头像
站长
· 阅读数 49

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

无论是执行setState(class组件)进行更新还是执行useState(function组件)进行更新,都会创建更新任务(update)并添加到fiber的更新队列上 (class组件的更新队列为updateQueue,function组件的更新队列为baseQueue)。更新任务创建好并关联到了fiber之后,接下来就进入react render 阶段的核心之一 -- reconciler 阶段。

reconciler 流程

reconciler 的过程可以分为四个阶段:

  1. 任务输入:scheduleUpdateOnFiber 是处理更新任务开始的地方

  2. 调度任务注册:与调度中心(Scheduler)交互,注册调度任务(scheduler task),等待回调

  3. 执行任务回调:执行渲染任务,在内存中构造出fiber树,同时与渲染器(react-dom)交互,在内存中创建出与fiber对应的DOM节点

  4. 输出DOM节点:与渲染器(react-dom)交互,渲染DOM节点

reconciler的运作流程是一个固定的流程,它的四个阶段可以用下图表示:

React源码解读之任务调度流程

任务输入

scheduleUpdateOnFiber

无论是首次渲染,还是后续更新操作,都会进入到react-reconciler对外暴露的updateContainer函数,然后在该函数中调用scheduleUpdateOnFiber,因此scheduleUpdateOnFiber函数是任务调度的入口,下面是该函数的关键代码:

// react-reconciler/src/ReactFiberWorkLoop.new.js

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {

  // 检查是否有循环更新
  // 避免例如在类组件 render 函数中调用了 setState 这种死循环的情况
  checkForNestedUpdates();

  // ...

  // 标记当前 current树的lane优先级及其子节点的lanes优先级,会自底向上更新 child.fiberLanes
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  if (enableUpdaterTracking) {
    if (isDevToolsPresent) {
      addFiberToLanesMap(root, fiber, lane);
    }
  }

  // Mark that the root has a pending update.
  // 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
  markRootUpdated(root, lane, eventTime);

  // ...

  // TODO: Consolidate with `isInterleavedUpdate` check
  if (root === workInProgressRoot) {
    
    // 在渲染过程中接收到一个更新,在根节点上标记一个交错更新,
    if (
      deferRenderPhaseUpdateToNextBatch ||
      (executionContext & RenderContext) === NoContext
    ) {
      workInProgressRootUpdatedLanes = mergeLanes(
        workInProgressRootUpdatedLanes,
        lane,
      );
    }
    if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
      
      // 由于有高优先级的新的 update,将当前的更新标记为暂停,并切换到新的 update
      markRootSuspended(root, workInProgressRootRenderLanes);
    }
  }

  // 执行可中断更新
  ensureRootIsScheduled(root, eventTime);

  // ...

  return root;
}

1、在 scheduleUpdateOnFiber 函数中,首先会调用 checkForNestedUpdates 方法,检查嵌套的更新数量,如果嵌套数量大于50 (NESTED_UPDATE_LIMIT = 50)层时,被认为是循环更新 (无限循环) 。此时会抛出异常,避免了例如在类组件render函数中调用了 setState 这种死循环的情况。

// 检查是否有循环更新
// 避免例如在类组件 render 函数中调用了 setState 这种死循环的情况
checkForNestedUpdates();

2、然后调用 markUpdateLaneFromFiberToRoot 方法,更新当前fiber节点lanes优先级及其父路径上父节点的child lanes优先级,即自底向上更新childLanes 。lanes的更新很简单,就是将当前任务的 lane 与 之前的 lane 进行二进制 或运算 叠加。

// 标记当前 current树的lane优先级及其子节点的lanes优先级,会自底向上更新 child.fiberLanes
const root = markUpdateLaneFromFiberToRoot(fiber, lane);

3、接着将update的lane通过二进制运算插入到 root.pendingLanes,标记root节点有一个待处理的更新。

// Mark that the root has a pending update.
// 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
markRootUpdated(root, lane, eventTime);

4、最后调用ensureRootIsScheduled,传入 root 节点和创建update的时间,开始执行可中断更新。

// 执行可中断更新
ensureRootIsScheduled(root, eventTime);

注册调度任务

安排调度任务 -- ensureRootIsScheduled

通过scheduleUpdateOnFiber函数输入任务后,会进入ensureRootIsScheduled函数。该函数的作用是为root安排调度任务。每个更新任务 (update) 都会经过 ensureRootIsScheduled 的处理。下面给出该函数的关键代码。

// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

  // ...

  // Schedule a new callback.
  // 重新安排一个新的渲染任务
  let newCallbackNode;
  // 如果新渲染任务的优先级是同步优先级
  // if 逻辑处理的是同步任务,同步任务不需经过 Scheduler
  if (newCallbackPriority === SyncLane) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue

    // 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数
    if (root.tag === LegacyRoot) {
      // LegacyRoot 启动模式
     
      // ...

      // 启动模式为 legacy模式
      // 把这个渲染任务加入 syncQueue 队列中
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      // 启动模式为非 legacy模式
      // 把这个渲染任务加入 syncQueue 队列中
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    
    // ...

  } else {

    // else 逻辑处理的是并发任务,并发任务需要经过 Scheduler

    //...

    // 根据调度优先级,调度并发任务
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  
  // ...

}

在 ensureRootIsScheduled 函数中,会根据任务的类型,去安排何种类型的调度任务。

  • 如果任务类型是同步任务,则不需要经过调度,经过scheduleLegacySyncCallback或scheduleSyncCallback包装,如果当前的 js 主线程空闲 (没有正在执行的react任务),则执行performSyncWorkOnRoot开始执行同步任务。

    // 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数 if (root.tag === LegacyRoot) { // LegacyRoot 启动模式

    // ...

    // 启动模式为 legacy模式 // 把这个渲染任务加入 syncQueue 队列中 scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root)); } else { // 启动模式为非 legacy模式 // 把这个渲染任务加入 syncQueue 队列中 scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); }

  • 如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务。

    // 根据调度优先级,调度并发任务 newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), );

创建调度任务 -- unstable_scheduleCallback

在调度器Scheduler中,首先会通过unstable_scheduleCallback函数创建调度任务(schedule task),然后根据任务是否超时,分别将任务插入到超时队列timerQueue 和调度任务队列taskQueue中。将任务插入调度任务队列taskQueue之后,会通过requestHostCallback函数去调度任务。

// packages/scheduler/src/forks/Scheduler.js

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 这个 currentTime 获取的是 performance.now() 时间戳
  var currentTime = getCurrentTime();

  // 任务开始的时间
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  // 根据调度优先级设置相应的超时时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  // 过期时间
  var expirationTime = startTime + timeout;

  // 属于 Scheduler 自己的 task
  var newTask = {
    id: taskIdCounter++,
    callback, // 这里的 callback 是 performConcurrentWorkOnRoot 函数
    priorityLevel, // 调度优先级
    startTime, // 任务开始时间
    expirationTime, // 任务过期时间
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // Scheduler调度任务时传入了 delay time,startTime 是大于 currentTime 的,
  // 表示这个任务将会延迟执行
  if (startTime > currentTime) {
    // 当前任务已超时,插入超时队列
    // This is a delayed task.
    newTask.sortIndex = startTime;
    // timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
    // 向二叉堆中添加节点
    push(timerQueue, newTask);
    // peek 查看堆的顶点, 也就是优先级最高的`task`
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // 这个任务是最早延迟执行的
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        // 取消现有的定时器
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // 安排一个定时器
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务未超时,插入调度任务队列
    newTask.sortIndex = expirationTime;
    // taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // 符合更新调度执行的标志
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // requestHostCallback 调度任务
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

值得注意的是,超时队列timerQueue 和调度任务队列taskQueue都是一个二叉堆结构,使用最小堆存储 Scheduler 的 task,使得在任务调度的过程中可以在O(1)的时间获取到优先级最高的task,提高了任务调度的效率。

实现调度任务 -- performWorkUntilDeadline

在创建调度任务时,如果任务没有超时,则会被插入到调度队列taskQueue中,然后会通过requestHostCallback函数去调度任务。requestHostCallback中的参数callback就是通过workLoop获取到的优先级最高的任务。

// 通过 postMessage,通知 scheduler 已经开始了帧调度
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

在实现调度任务时,react 通过 new MessageChannel(); 创建了消息通道,当发现 js 线程空闲时,通过 postMessage 通知 Scheduler 开始调度。然后 react 接收到调度开始的通知时,就通过 performWorkUntilDeadline 函数去执行任务,从而实现了帧空闲时间的任务调度。

// 获取当前设备每帧的时长
function forceFrameRate(fps) {
  // ...
  
  if (fps > 0) {
    frameInterval = Math.floor(1000 / fps);
  } else {
    // reset the framerate
    frameInterval = frameYieldMs;
  }
}

// 帧结束前执行任务
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;
    const hasTimeRemaining = true;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `scheduledHostCallback` errors, then
    // `hasMoreWork` will remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
        // 如果还有调度任务就执行
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  // 通过 MessageChannel 创建消息通道,实现任务调度通知
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

// 通过 postMessage,通知 scheduler 已经开始了帧调度
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

执行任务回调

执行任务回调,实际上就是执行 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 。如果是同步任务,则调用performSyncWorkOnRoot函数执行同步任务,如果是并发任务,则任务需要经过调度,调用performConcurrentWorkOnRoot函数,执行并发任务(调度任务)。

执行同步渲染任务 -- performSyncWorkOnRoot

任务类型为同步任务,并且当前的js线程空闲(没有正在执行的react任务),通过performSyncWorkOnRoot(root)方法执行同步渲染任务。

// react-reconciler/src/ReactFiberWorkLoop.new.js

// 这个函数是不经过Scheduler调度的同步任务的入口点
function performSyncWorkOnRoot(root) {
  
  // ...

  // 从根节点开始进行同步渲染任务
  let exitStatus = renderRootSync(root, lanes);
  
  // ...

  // We now have a consistent tree. Because this is a sync render, we
  // will commit it even if something suspended.
  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 提交fiber树
  commitRoot(root);

  // Before exiting, make sure there's a callback scheduled for the next
  // pending level.
  // 为 root 节点调度同步渲染任务
  ensureRootIsScheduled(root, now());

  return null;
}

在 performSyncWorkOnRoot 函数中,主要做了三件事:

  • 调用 renderRootSync 从根节点开始执行同步渲染任务

  • 调用 commitRoot 提交fiber树,执行commit流程

  • 继续调用 ensureRootIsScheduled,为根节点安排同步渲染任务

执行并发任务(调度任务) -- performConcurrentWorkOnRoot

任务类型不是同步任务时,通过 performConcurrentWorkOnRoot (root, didTimeout)

执行并发渲染任务,去执行可中断的更新。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 并发任务的入口,即需要通过调度
function performConcurrentWorkOnRoot(root, didTimeout) {

  // ...
  // 时间切片
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    // 独立于 includesBlockingLane 的检查,因为在渲染已经开始后,车道可能会过期
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);

  // 退出状态
  // 默认情况下并发更新总是使用时间切片
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes) // 并发渲染
    : renderRootSync(root, lanes); // 同步渲染

  // ...

  // 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
  ensureRootIsScheduled(root, now());
  // 下一个要渲染的任务,递归调用 performConcurrentWorkOnRoot自身 继续执行调度
  if (root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    // 渲染被阻断, 返回一个新的performConcurrentWorkOnRoot函数, 等待下一次调用
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

在调用 performConcurrentWorkOnRoot 执行并发任务(调度任务)时,会根据 shouldTimeSlice 来决定是执行同步渲染任务还是执行并发渲染任务。

在react中,只有并发渲染任务才会使用 time slice(时间切片),因此进入renderRootConcurrent执行可中断更新。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
 
  // ...

  // 循环执行并发渲染任务
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  // ...
}

执行可中断更新

在执行并发渲染任务时,performConcurrentWorkOnRoot 会执行以下过程:

performConcurrentWorkOnRoot => renderRootConcurrent => workLoopConcurrent 。workLoopConcurrent 在每次对 workInProgress 执行 performUnitOfWork 之前,都会先判断 shouldYield() 的值。若为true,则中断执行,若为false,则继续执行。shouldYield() 停顿机制,实现了时间切片和可中断渲染。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  // shouldYield() 停顿机制,这个机制实现了 时间切片和可中断渲染
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

执行同步更新 (不可中断)

在执行同步渲染任务时,则会进入 renderRootSync 函数,循环调用 workLoopSync 进行同步更新。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function renderRootSync(root: FiberRoot, lanes: Lanes) {

  // ...

  // 循环执行同步渲染任务
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

  // ...

}

在 workLoopSync 中,只要 workInProgress (workInProgress fiber 树中新创建的 fiber 节点) 不为 null,就会一直循环,执行 performUnitOfWork,进行更新。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork

无论是执行同步渲染任务(不需经过调度),还是执行并发渲染任务(需经过调度),都会进入 performUnitOfWork 函数中。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function performUnitOfWork(unitOfWork: Fiber): void {

  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    // 执行当前任务,返回下一个任务
    next = beginWork(current, unitOfWork, subtreeRenderLanes);

    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    // 执行当前任务,返回下一个任务
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    // 没有下一个任务了,完成当前的渲染任务
    completeUnitOfWork(unitOfWork);
  } else {
    // workInProgress 是一个全局变量,如果 workInProgress 不为 null,
    // 在 workLoopSync 和 workLoopConcurrent 中循环执行任务,直到所有的任务执行完
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

在 performUnitOfWork 函数中,会以 fiber 作为单元,进行reconciler过程。执行 beginWork,完成当前fiber上的更新任务,然后返回下一个fiber,更新 workInProgress,从而响应了上面的 workLoop循环 (workLoopSync 和 workLoopConcurrent),直到所有的更新任务执行完。

beginWork

在 performUnitOfWork 中调用的 beginWork,是根据当前的执行环境,封装调用了 ReactFiberBeginWork.new.js 中的 beginWork 函数。如果是 __DEV__ 环境,则再次封装 originalBeginWork 函数,如果是非 __DEV__ 环境,则直接调用 originalBeginWork 函数。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

import { beginWork as originalBeginWork } from './ReactFiberBeginWork.new';


let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
  const dummyFiber = null;
  beginWork = (current, unitOfWork, lanes) => {
      
    // ...

    try {
      return originalBeginWork(current, unitOfWork, lanes);
    } catch (originalError) {
      // ...
    }
  };
} else {
  beginWork = originalBeginWork;
}

在 originalBeginWork 函数中 (ReactFiberBeginWork.new.js 中的 beginWork 函数),会根据 workInProgress 的 tag 属性,执行不同类型的React元素的更新函数。

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes,
  ): Fiber | null {
   
    // ...

    // 根据 workInProgress 的 tag 属性,
    // 执行不同类型React元素的更新函数
    switch (workInProgress.tag) {
      
        // ...
      case FunctionComponent: {
        const Component = workInProgress.type;
        const unresolvedProps = workInProgress.pendingProps;
        const resolvedProps =
          workInProgress.elementType === Component
            ? unresolvedProps
            : resolveDefaultProps(Component, unresolvedProps);
        return updateFunctionComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
        );
      }
      case ClassComponent: {
        const Component = workInProgress.type;
        const unresolvedProps = workInProgress.pendingProps;
        const resolvedProps =
          workInProgress.elementType === Component
            ? unresolvedProps
            : resolveDefaultProps(Component, unresolvedProps);
        return updateClassComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
        );
      }
     
      // ...

    }
  
    // ...
}

无论是何种类型的React元素,其更新函数最终都会调用 reconcileChildren 函数。以 updateFunctionComponent 为例,首先执行 renderWithHooks 获取children,然后调用 reconcileChildren执行协调过程。

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {

  // ...

  if (__DEV__) {

    // ...

  } else {

    // 执行 renderWithHooks,获取children
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
  }

  // ...

  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // 调用 reconcileChildren,执行协调过程
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

reconcileChildren 做的事情是React的另一核心之一 -- diff 过程,会在下一篇文章中详细解析。

completeUnitOfWork

执行 beginWork,完成当前fiber上的更新任务后,如果返回的下一个 fiber 为 null,表示当前任务的fiber树已经遍历完了,此时 workInProgress 也变为了null,就进入 completeUnitOfWork 执行渲染任务。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function completeUnitOfWork(unitOfWork: Fiber): void {

  // 尝试完成当前的工作单元,然后移动到下一个兄弟。 如果没有更多的兄弟姐妹,则返回到父节点。
  let completedWork = unitOfWork;
  do {

    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentDebugFiberInDEV(completedWork);
      let next;
      if (
        !enableProfilerTimer ||
        (completedWork.mode & ProfileMode) === NoMode
      ) {
        // 完成当前的渲染任务,返回下一个任务
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        // 完成当前的渲染任务,返回下一个任务
        next = completeWork(current, completedWork, subtreeRenderLanes);
        // Update render duration assuming we didn't error.
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      resetCurrentDebugFiberInDEV();

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        // workInProgress 是一个全局变量,如果 workInProgress 不为 null,
        // 在 workLoopSync 和 workLoopConcurrent 中循环执行任务,直到所有的任务执行完
        workInProgress = next;
        return;
      }
    } else {
      // This fiber did not complete because something threw. Pop values off
      // the stack without entering the complete phase. If this is a boundary,
      // capture values if possible.
      // fiber节点没有完成渲染,从堆栈中弹出
      const next = unwindWork(completedWork, subtreeRenderLanes);

     // ...

    }

    // 当前的fiber已完成渲染任务,移动到它的兄弟节点,继续执行兄弟节点上的任务
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    // 当前的fiber已完成渲染任务,回到它的父节点,继续执行父节点上的任务
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // ...
}

completeUnitOfWork 做的事情,就是从当前的fiber节点开始,到其兄弟节点,再到其父节点,逐层往上完成fiber树上的渲染任务。completeUnitOfWork 结束后,后面就到了commit阶段,即将fiber树输出反映到DOM上。

输出DOM节点

在输出阶段,commitRoot 只是设置了更新的优先级,主要的实现逻辑是在 commitRootImpl 函数中。其主要做的事情,就是处理副作用队列,将最新的fiber树结构反映到DOM上。

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function commitRootImpl(root, renderPriorityLevel) {

  // do while 循环,执行所有的副作用
  do {

    // flushPassiveEffects 在最后会调用 flushSyncUpdateQueue
    // 循环执行 flushPassiveEffects,直到没有挂载阶段的副作用
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

 

  // ...

  if (subtreeHasEffects || rootHasEffect) {

    // 有副作用,处理 fiber树上的副作用

    // ...

    // 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的 host tree 的state
    // 这个阶段是 生命周期函数getSnapshotBeforeUpdate 调用的地方
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    // ...

    // The next phase is the mutation phase, where we mutate the host tree.
    // 下一个阶段是 mutation phase,可以在这个阶段 改变 host tree
    commitMutationEffects(root, finishedWork, lanes);

    // ...
   
    // 下一个阶段是 layout phase(布局阶段)
    // 提交 layout 阶段的副作用
    commitLayoutEffects(finishedWork, root, lanes);
    
    // ...

    // Tell Scheduler to yield at the end of the frame, so the browser has an
    // opportunity to paint.
    // 告诉调度器在帧结束时让出,这样浏览器就有机会进行绘制。
    requestPaint();

    // 重置执行栈环境
    executionContext = prevExecutionContext;

    // Reset the priority to the previous non-sync value.
    // 将优先级重置为之前的 非同步优先级
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  } else {

    // 没有副作用

    // ...
  }

  // ...
  // 在退出 commitRoot 之前总是调用 ensureRootIsScheduled(),确保 root 节点上的任何额外任务都被调度
  ensureRootIsScheduled(root, now());

  // ...

  // If layout work was scheduled, flush it now.
  // 布局工作已安排,立即刷新它
  flushSyncCallbacks();

  // ...
}

commitRootImpl 的核心处理逻辑可分为3个阶段:

  • 阶段一:commitBeforeMutationEffects

这个阶段主要是处理副作用队列中带有Snapshot、Passive标记的fiber节点,在这个阶段可以读取改变之前的 host tree 的state,这个阶段也是 生命周期函数 getSnapshotBeforeUpdate 调用的地方。

  • 阶段二:commitMutationEffects

这个阶段可以改变host tree,重置current树,主要是处理副作用队列中带有Placement、Update、Deletion、Hydrating标记的fiber节点。

  • 阶段三:commitLayoutEffects

这个阶段是布局阶段,主要处理副作用队列中带有Update | Callback标记的fiber节点,然后进行布局。

总结

本文介绍了 reconciler 的运作流程,将其分为了 4 个阶段,并深入分析了各个阶段的运作。

  • 第一个阶段是任务输入阶段,scheduleUpdateOnFiber函数是任务输入的入口。

  • 第二个阶段是调度任务注册阶段,通过与调度中心(Scheduler)交互,通过unstable_scheduleCallback函数注册调度任务。

  • 第三个阶段是执行任务回调阶段,主要是通过performSyncWorkOnRoot执行同步渲染任务,通过performConcurrentWorkOnRoot执行并发渲染任务。

  • 第四个阶段是输出DOM节点阶段,在内存中构建好的fiber树,与渲染器(react-dom)交互,渲染DOM节点。

这4个阶段,基本覆盖了react-reconciler包的核心逻辑。

转载自:https://juejin.cn/post/7057144903054131213
评论
请登录