preface

Because the space is too long, I will Lane model source code analysis is divided into two parts. The React source code is used to parse the priority Lane model.

How does Lane work during the mission

Last time, we analyzed how Lane dealt with the problem of task hunger and queue-jumping, and then handed the task to Scheduler for task scheduling.

When the Scheduler start task scheduling, will cycle taskQueue mission (please see the React source code parsing Scheduler) in detail, then will execute performConcurrentWorkOnRoot function.

We then look at the performConcurrentWorkOnRoot function in the source code:

function performConcurrentWorkOnRoot(root, didTimeout) {...// From all the tasks to be performed, find the task with the highest priority
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  if (lanes === NoLanes) {
    // Defensive coding. This is never expected to happen.
    return null;
  }

  // shouldTimeSlice determines whether to render in concurrent or synchronous mode according to the priority of lane.
  // didTimeout Determines whether the current task has timed out
  letexitStatus = shouldTimeSlice(root, lanes) && (disableSchedulerTimeoutInWorkLoop || ! didTimeout) ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes); .return null;
}
Copy the code

You can see that the getNextLanes method is called again in this function. Why is it called again? If the priority of this new task is lower than that of the task to be executed, the rendering will continue. However, if the priority of this new task is higher than that of the task to be executed, the task with higher priority needs to be executed first. The getNextLanes method is called every time a task is executed to ensure that the highest priority task is executed at all times.

Solve task hunger

letexitStatus = shouldTimeSlice(root, lanes) && (disableSchedulerTimeoutInWorkLoop || ! didTimeout) ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);Copy the code

This judgment is the key to solving task hunger.

DidTimeout Indicates whether the current task is expired, and if it is, the task will be executed in synchronous mode.

And then let’s look at shouldTimeSlice.

export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {

  // Check whether the current task's lane is in expired Lanes
  // If yes, to prevent starvation, false is returned and synchronous rendering is performed
  if((lanes & root.expiredLanes) ! == NoLanes) {return false;
  }

  // Check whether concurrent mode is enabled and whether the current render mode is in concurrent mode
  // If so, return true and use concurrent rendering
  if( allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) ! == NoMode ) {return true;
  }

  // Check whether the current lane has an intersection with SyncDefaultLanes
  // If so, synchronous rendering mode will be enabled; otherwise, concurrent rendering mode will be used
  // InputContinuousHydrationLane InputContinuousLane DefaultHydrationLane DefaultLane
  // All four lanes need to be executed in synchronous mode
  const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane;

  return (lanes & SyncDefaultLanes) === NoLanes;
}
Copy the code

Remember we said about the task of hunger before processing, the main logic in markStarvedLanesAsExpired function, its main role is to add the expiration time for the current task according to the priority, and check whether there is a task in the did not perform the task of expired, If a task expires, add lane of expiredLanes to expiredLanes. The task will be executed in synchronous mode to avoid starvation.

In shouldTimeSlice, it checks to see if the lane of the current task is in expiredLanes that are out of date, and if so, to prevent starvation issues, it returns false and performs synchronous rendering.

If not, it indicates that the current task has not expired, then it checks whether concurrent mode is enabled and whether the current rendering mode is in concurrent mode:

if( allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) ! == NoMode ) {return true;
  }
Copy the code

If the condition is met, return true and use concurrent mode.

If not, the current lane is checked to see if it intersects with SyncDefaultLanes:

const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane;
    
return (lanes & SyncDefaultLanes) === NoLanes;
Copy the code

The four lanes in SyncDefaultLanes are all executed in synchronous rendering mode, and if the current lane is the same as one of these four, synchronous rendering mode is enabled, otherwise concurrent rendering is used.

Tasks executed in synchronous mode cannot be interrupted until completion. The task hunger problem has been solved accordingly.

By default, we use concurrent mode to execute tasks.

Status updates

Execute tasks in concurrent mode, then execute renderRootConcurrent:

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {... prepareFreshStack(root, lanes);do {
    try {
      workLoopConcurrent();
      break;
    } catch(thrownValue) { handleError(root, thrownValue); }}while (true); .Copy the code

In this function, we mainly look at two places, one is the prepareFreshStack method, and the other is the workLoopConcurrent function.

Let’s start with the prepareFreshStack method:

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
  root.finishedWork = null; root.finishedLanes = NoLanes; . workInProgressRoot = root; workInProgress = createWorkInProgress(root.current,null); workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; . workInProgressRootSkippedLanes = NoLanes; workInProgressRootUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; }Copy the code

The main purpose of this method is to create a workInProgress tree:

workInProgress = createWorkInProgress(root.current, null);
Copy the code

Then will be assigned to the task priority workInProgressRootRenderLanes, subtreeRenderLanes, workInProgressRootIncludedLanes these three variables.

WorkInProgressRootRenderLanes indicates whether the current task being performed and value said to have tasks are performed, not conversely in task execution.

SubtreeRenderLanes represent lanes of the fiber nodes that need to be updated. This value will be used to determine whether the fiber nodes need to be updated.

Next step is to traverse the Fiber tree and update the fiber node:

  workLoopConcurrent();
          |
          v
  performUnitOfWork(workInProgress);
Copy the code

The workLoopConcurrent function mainly calls the performUnitOfWork function. Let’s look at the performUnitOfWork function directly:

function performUnitOfWork(unitOfWork: Fiber) :void {...constcurrent = unitOfWork.alternate; .letnext; next = beginWork(current, unitOfWork, subtreeRenderLanes); . unitOfWork.memoizedProps = unitOfWork.pendingProps;if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else{ workInProgress = next; }... ReactCurrentOwner.current =null;
}
Copy the code

The beginWork function is called in the performUnitOfWork function. The beginWork function determines whether the current fiber node needs to be updated:


function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) :Fiber | null {...if(current ! = =null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // didReceiveUpdate indicates whether there are new props updates, set to true if there are, and false if there are no
    if( oldProps ! == newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type ! == current.type :false)
    ) {
      didReceiveUpdate = true;
    } else {
      / / checkScheduledUpdateOrContext function checks the current fiber node whether it exists in renderLanes lanes
      If the fiber node exists, the current fiber node needs to be updated. If the fiber node does not exist, the current fiber node needs to be updated
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // No pending updates or context. Bail out now.
        didReceiveUpdate = false;
        // Reuse the previous node
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      if((current.flags & ForceUpdateForLegacySuspense) ! == NoFlags) { didReceiveUpdate =true;
      } else {
        didReceiveUpdate = false; }}}else {
    didReceiveUpdate = false; }... }Copy the code

If the old props are the same as the new props, it needs to be updated. If the old props are not, it needs to be updated. If the lanes on the current fiber node are renderLanes, it does not need to be updated. Call the checkScheduledUpdateOrContext method to judge whether need to update:

function checkScheduledUpdateOrContext(current: Fiber, renderLanes: Lanes,) :boolean {
  const updateLanes = current.lanes;
  if (includesSomeLane(updateLanes, renderLanes)) {
    return true;
  }
 
  if (enableLazyContextPropagation) {
    const dependencies = current.dependencies;
    if(dependencies ! = =null && checkIfContextChanged(dependencies)) {
      return true; }}return false;
}
Copy the code

Can see checkScheduledUpdateOrContext method in judging updateLanes and renderLanes intersection, if you have it returns true, not false.

Back we looked down again, when checkScheduledUpdateOrContext returns false, said don’t need to be updated, will call attemptEarlyBailoutIfNoScheduledUpdate function of nodes before reuse.

Return true to determine the type of component based on the tag on the fiber node.

switch (workInProgress.tag) {
    ...
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      returnupdateClassComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); }... }Copy the code

It then executes the updateClassComponent method, which we won’t parse in detail here, but will do so in fiber.

When we enter the updateClassComponent method, we call the updateClassInstance method because we are doing updates:

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {... shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderLanes, ); . }Copy the code

The updateClassInstance method is used to determine whether the state needs to be updated based on the lane on the update object, and to call some pre-render lifecycle: getDerivedStateFromProps, componentWillUpdate, etc.

So let’s see how the lane part does the update judgment.

In updateClassInstance, a method called processUpdateQueue is called. Let’s look at lane:

export function processUpdateQueue<State> (workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,) :void {...let update = firstBaseUpdate;
    do {
      const updateLane = update.lane;
      const updateEventTime = update.eventTime;
      // Determine if the lane mounted on the update object is in renderLanes. If so, the update object needs to be updated
      // If not, the update object is not needed and can be reused directly
      if(! isSubsetOfLanes(renderLanes, updateLane)) {const clone: Update<State> = {
          eventTime: updateEventTime,
          lane: updateLane,

          tag: update.tag,
          payload: update.payload,
          callback: update.callback,

          next: null};if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }
        newLanes = mergeLanes(newLanes, updateLane);
      } else {
        if(newLastBaseUpdate ! = =null) {
          const clone: Update<State> = {
            eventTime: updateEventTime,
            lane: NoLane,

            tag: update.tag,
            payload: update.payload,
            callback: update.callback,

            next: null}; newLastBaseUpdate = newLastBaseUpdate.next = clone; } newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, );const callback = update.callback;
        if( callback ! = =null&& update.lane ! == NoLane ) { workInProgress.flags |= Callback;const effects = queue.effects;
          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
        }
      }
      update = update.next;
      if (update === null) {
        pendingQueue = queue.shared.pending;
        if (pendingQueue === null) {
          break;
        } else {
          const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
          lastPendingUpdate.next = null;
          update = firstPendingUpdate;
          queue.lastBaseUpdate = lastPendingUpdate;
          queue.shared.pending = null; }}}while (true); . }Copy the code

This code loops out the update object in the current Fiber node, and then compares the lane mounted on the update object with renderLanes to determine whether the lane on the update object exists in renderLanes. If so, If the current update object does not exist, the current update object will be reused and the update object will be skipped.

Consumption of lane

Once the loop has traversed the workInProgress tree and the commit phase is started, let’s look at the key code:

function commitRootImpl(root, renderPriorityLevel) {...letremainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); markRootFinished(root, remainingLanes); . }Copy the code

Merge finishedwork. lanes and finishedwork. childLanes to obtain the remaining lanes that need to be updated. Then call markRootFinished to empty the data of the lanes that have been executed. Mount the remaining Lanes to pendingLanes in preparation for the next execution:

export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
  // Remove unexecuted lanes from pendingLanes, and you will find lanes that have been executed
  const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;

  // Mount the remaining Lanes to pendingLanes in preparation for the next execution
  root.pendingLanes = remainingLanes;

  root.suspendedLanes = 0;
  root.pingedLanes = 0;

  // Delete executed lanes from expiredLanes, mutableReadLanes, and entangledLanes
  root.expiredLanes &= remainingLanes;
  root.mutableReadLanes &= remainingLanes;

  root.entangledLanes &= remainingLanes;

  if (enableCache) {
    const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
    if (pooledCacheLanes === NoLanes) {
      root.pooledCache = null; }}const entanglements = root.entanglements;
  const eventTimes = root.eventTimes;
  const expirationTimes = root.expirationTimes;

  // Fetch the lanes that have been executed and empty all their data
  // eventTimes event trigger time, expirationTimes task expiration time, etc
  let lanes = noLongerPendingLanes;
  while (lanes > 0) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1<< index; entanglements[index] = NoLanes; eventTimes[index] = NoTimestamp; expirationTimes[index] = NoTimestamp; lanes &= ~lane; }}Copy the code

When the commit stage is complete and the current task is successfully executed, a final call to the ensureRootIsScheduled function is required to ensure remainingLanes are not null and the remaining tasks continue.

To this, lane source code general process is also combed almost, the end, again refill…