Interpretation of React's source code and principles (9): Lanes

Written at the beginning of the column (stack armor)

  1. The author is not an expert in front-end technology, but just a novice in front-end technology who likes to learn new things. He wants to learn source code just to cope with the rapidly declining front-end market and the need to find a job. This column is the author's own thinking in the process of learning And experience, there are also many parts that refer to other tutorials. If there are errors or problems, please point them out to the author. The author guarantees that the content is 100% correct. Please do not use this column as a reference answer.

  2. The reading of this column requires you to have a certain foundation of React, JavaScript and front-end engineering. The author will not explain many basic knowledge points, such as: what is babel, what is the syntax of jsx, please refer to it when necessary material.

  3. Many parts of this column refer to a large number of other tutorials. If there are similarities, the author plagiarized them. Therefore, this tutorial is completely open source. You can integrate, summarize and add your own understanding to various tutorials as the author.

Contents of this section

In the previous chapters, we mainly analyzed how the **Scheduler** system schedules our rendering tasks. It is mainly used to judge when we want to return the process to us during the execution of a batch of tasks. browser; and in this article we will talk about a priority system in our React—— lanes This system is mainly used to discuss whether a task with a low priority is interrupted during the execution process, and instead Execute higher priority tasks.

Definition of Lanes

Let's first take a look at the definition of this lane, which is in this position of the code: packages/react-reconciler/src/ReactFiberLane.new.js:

It can be seen that lanes use 31-bit binary to represent priority lanes, a total of 31 lanes, the smaller the number (the farther to the right the position of 1) is, the higher the priority is. In actual use, we will have a 31-bit binary number to identify our task. If one of his bits corresponds to 1, then there is a task for a certain priority of him. Otherwise, it is idle. You can It is understood that we regard the task as my car, they need to drive in different lanes so as not to affect each other, but the right lane can overtake to the left (high priority). That's why we call it the lane model .

Note: In the following, we will use lane to represent a single priority, that is, a certain lane occupied by a task; lanes represent all our lanes, and each lane in lanes corresponds to a lane

// lane使用31位二进制来表示优先级车道共31条, 位数越小(1的位置越靠右)表示优先级越高
export const TotalLanes = 31;

// 没有优先级
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

// 同步优先级,表示同步的任务一次只能执行一个,例如:用户的交互事件产生的更新任务
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

// 连续触发优先级,例如:滚动事件,拖动事件等
export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;

// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;

// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级(React18 的新机制)
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /*                       */ 0b0000000001000000000000000000000;

// 重试车道
const RetryLanes: Lanes = /*                            */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /*                             */ 0b0000100000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;
// 可选的车道
export const SelectiveHydrationLane: Lane = /*          */ 0b0001000000000000000000000000000;

//非空闲车道
const NonIdleLanes: Lanes = /*                          */ 0b0001111111111111111111111111111;
// 空闲车道
export const IdleHydrationLane: Lane = /*               */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0100000000000000000000000000000;
// 屏幕外车道
export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

Operation of Lanes

The reason why lane is designed in this way is that it is convenient for us to use bit operations to achieve some functions such as comparison, merging, and reset. Let's take a look at some commonly used operations, which will be involved in the follow-up:

  • generate lane

We can use the left shift operation to quickly generate a corresponding lane. For example, if we want to generate one DefaultLane, we only need to use the following code:

const lane = 1 << 5;
  • filter lanes

Sometimes we need to judge which lanes already have tasks and which lanes have no tasks. At this time, we need to use our lanes and some of our predefined lanes content to perform a bitwise AND operation, and the obtained content is which of our corresponding lanes There are tasks, for example, we now want to know which lanes in the non-idle lanes already have tasks, we only need to use NonIdleLanesthe bitwise AND operation with our lanes

const nonIdlePendingLanes = pendingLanes & NonIdleLanes;

If we want to filter out tasks that do not contain certain lanes, we only need to invert the filter model bitwise and then bitwise AND:

const IdlePendingLanes = pendingLanes & ~NonIdleLanes;
  • merge lanes

If we merge our lanes into lanes, we only need to perform a bitwise OR operation, and all parts marked as 1 will be automatically merged, which is used to merge the lanes used by our current task into the total lanes :

root.pendingLanes |= updateLan
  • Take out the used lane with the highest priority

Take out the task with the highest priority, which is the rightmost 1 in the binary. The operation is a bitwise AND operation between lanes and its own negative number, because our negative number is stored using the complement, if the last bit is 1, then the complement will become 1, and the AND operation will return 1; if the last The bit is 0, the complement is 0, but a carry of one bit will be generated, and this carry will continue until it encounters 0, and this 0 is the complement of 1, and 1 will be generated at this time, and the AND operation will return 1 , you can write a few numbers to push this conclusion yourself:

lanes & -lanes;
  • Delete the corresponding lane

We sometimes take out the lane we need from our total lanes and then reset it. We can invert the value of the specified lane bit by bit, and then perform bitwise AND operation with our lanes. At this time, except what we need That lane is 0 and others are 1. When doing bitwise AND, the lane we reset must become 0, while the other lanes will retain their original values:

lanes &= ~lane;

Lanes in Fiber

Before we start the formal explanation, let's take a look at the lanes-related structure on Fiber that we omitted earlier, which is closely related to the entire process we want to explain:

First, there are two structures on the Fiber node:

Lanes stores the lanes of this node, and childLanes stores the lanes of child nodes. Through this structure, we pass our update tasks layer by layer. We can quickly know whether our subtree is through this childLanes need to be updated

lanes: Lanes,
childLanes: Lanes,

After that is our FiberRoot node. Let's take a look at some of the attributes related to lanes. Here you only need a rough image. We will talk about it later. SuspencedLanes and pingedLanes are used for suspense type components Yes, that is, asynchronous rendering. If you are interested, you can find out by yourself. We will not go into details. We mainly use the two fields of pendingLanes and expiredLanes:

callbackPriority = NoLane;             // 传入的回调函数的优先级
eventTimes = createLaneMap(NoLanes);   // 每个lane 的事件发生的事件
pendingLanes = NoLanes                 // 即将被处理的 lanes 列表。
suspencedLanes = NoLanes;              // 被挂起的 lanes 列表,React 通过 suspense 和 lazy 这两个功能来支持异步渲染
pingedLanes = NoLanes;                 // 记录需要重新安排或处理的 lanes ,和上面的 suspencedLanes 配合使用
expiredLanes = NoLanes;                // 表示过期任务的 lanes
mutableReadLanes = NoLanes;            // 可变状态的优先级的 lanes
entangledLanes = NoLanes;              // 当前正在执行的任务需要等待哪些任务完成

Judgment of Event Lane

So now let's see how the lane is determined, that is, when to use which lane, let's go back to createRoot first, there is a function in createRootlistenToAllSupportedEvents , its function is to bind all supported events.

// 绑定所有可支持的事件
listenToAllSupportedEvents(rootContainerElement);

Let's take a look at the logic, we only look at the lanes part, which enters listenToNativeEventthis function, and this function calls addTrappedEventListenerthis function; this function calls createEventListenerWrapperWithPriority, we will take a closer look at it later

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
    
    
   //... 省略
   listenToNativeEvent(domEventName, true, rootContainerElement);
}
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget,
): void {
    
    
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
    
    
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
}

createEventListenerWrapperWithPriorityThis function calls getEventPrioritythis function, which sets the priority of the incoming events according to the category. This priority corresponds to the priority in our lane one by one:

  • The user's click, input box input, etc. are all set to synchronization priority. This is because the user needs to get feedback immediately when operating. If there is no feedback after the operation, the user will feel that the interface is stuck.
  • For scrolling events, dragging events and other continuously triggered events, we set the priority of continuous triggering
  • If it is a message event, we determine the priority of the task by calling the priority of the current task
  • Other events are the default priority

After obtaining the priority, the callback function corresponding to the priority will be based on the priority of the obtained event

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
    
    
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  // 设置回调函数
  switch (eventPriority) {
    
    
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

// 离散事件优先级,例如:点击事件,input输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane

//获取事件的优先级
export function getEventPriority(domEventName: DOMEventName): * {
    
    
  switch (domEventName) {
    
    
    case 'cancel':
    case 'click':
    //... 省略
    case 'selectstart':
      return DiscreteEventPriority;
    case 'drag':
    //... 省略
    case 'pointerleave':
      return ContinuousEventPriority;
    case 'message': {
    
    
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
    
    
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          return DefaultEventPriority;
        case IdleSchedulerPriority:
          return IdleEventPriority;
        default:
          return DefaultEventPriority;
      }
    }
    default:
      return DefaultEventPriority;
  }
}

Lane acquisition

In the above, we have mentioned that different events will be judged as different lanes by React and bound to this event, so how does this priority work? Let’s start with the creation of an update. Let’s take a look first. What happens when one of our events calls setState to create an update:

First when we setStatecall , we call enqueueSetStatethis function:

Component.prototype.setState = function(partialState, callback) {
    
    
  // ... 省略
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

enqueueSetStateCreate an update for the fiber corresponding to the event, and then add the update object to the updateQueue update queue. This part should be very familiar to readers who have read the previous tutorials. It is basically consistent with the logic updateContainerin Let's talk about the priority part in detail. In this article, we specifically call here requestUpdateLaneto get the priority

  enqueueSetState(inst, payload, callback) {
    
    
    // 获得 fiber
    const fiber = getInstance(inst);
    // 获取当前事件触发的时间
    const eventTime = requestEventTime();
    // 获取到当前事件对应的 Lane
    const lane = requestUpdateLane(fiber);
    // 创建更新对象
    const update = createUpdate(eventTime, lane);
    // 挂载
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
    
    
      //省略 DEV 模式代码
      update.callback = callback;
    }
      
	// 将更新对象添加进更新队列中
    enqueueUpdate(fiber, update, lane);
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
    
    
      entangleTransitions(root, fiber, lane);
    }  
    // ....
    if (enableSchedulingProfiler) {
    
    
      markStateUpdateScheduled(fiber, lane);
    }
  },

requestUpdateLaneThe function of the function is to judge its priority according to the current mode and the current event. Its logic from top to bottom is as follows:

  • First judge whether it is synchronous mode (React 18 defaults to concurrent mode), synchronous mode does not need to judge the priority, and directly returns the synchronous priority
  • Then judge whether there is a task being executed, that is, judge workInProgressRootRenderLaneswhether it empty. We will mention this in detail later, if there is any, directly return its priority
  • Then determine whether it is a transition priority. This is a new mechanism added by React 18. TransitionIts function is to manually reduce the priority of some update tasks, so that it can be triggered later to ensure that some events on the page can get feedback even if it does. Combined with the above introduction, we can see that Transitionthe priorities are all after the default priority, that is, the part with lower priority.
  • The logic of transition priority assignment is that we first use the transition priority TransitionLane1 with the highest priority. If this priority already exists, then we continue to use TransitionLane2 until all 16 TransitionLanes are used up, and then restart from the first TransitionLane1 start using
  • If it is not a transition priority, it means that we are in an idle state and we need to start a task normally. We take out the above and operate according to the priority set by the event we bound
  • If it is not triggered by our event-bound priority, then determine that it is triggered by an external event, such as setTimeout, etc., to obtain its set priority
export function requestUpdateLane(fiber: Fiber): Lane {
    
    
  // 获取到当前渲染的模式:sync mode(同步模式) 还是 concurrent mode(并发模式)
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    
    
    // 同步模式
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    
    
    //并发模式,判断是否有任务正在执行,workInProgressRootRenderLanes是在初始化workInProgress树时,将当前执行的任务的优先级赋值给了workInProgressRootRenderLanes,如果 workInProgressRootRenderLanes 不为空说明有任务在执行了,那么则直接返回这个正在执行的任务的lane,
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }
  // 是否是过渡优先级,如果是的话,则返回一个过渡优先级
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    
    
    if (__DEV__ && ReactCurrentBatchConfig.transition !== null) {
    
    
      const transition = ReactCurrentBatchConfig.transition;
      if (!transition._updatedFibers) {
    
    
        transition._updatedFibers = new Set();
      }

      transition._updatedFibers.add(fiber);
    }
    if (currentEventTransitionLane === NoLane) {
    
    
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }

  // 获取我们上文提到的绑定的不同事件的对应优先级
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    
    
    return updateLane;
  }
  // 在react的外部事件中触发的更新事件,比如:setTimeout等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

The role of Lane in the schedule phase

In the above, we obtained a lane of an update event, and then we will take a closer look at how this acquired lane plays its role:

We continue to look down along enqueueSetStatethe function . After getting the lane, we need to create an update object and put it in the new queue. Then we call scheduleUpdateOnFiberthe function. We have mentioned this function once before, but we just mentioned it Let's take a look at the rendering logic. Now let's add priority-related content and look at this function:

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
    
    
  // 检查是否做了无限循环更新,弱是则抛出异常
  checkForNestedUpdates();

  // 自底向上更新整个优先级
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    
    
    return null;

  // 将当前需要更新的lane添加到fiber root的pendingLanes属性上,表示有新的更新任务需要被执行
  // 通过计算出当前lane的位置,并添加事件触发时间到eventTimes中
  markRootUpdated(root, lane, eventTime);
  // ....
  ensureRootIsScheduled(root, eventTime);
  // ....
  return root;
}

First we call markUpdateLaneFromFiberToRootthis function, its function is to update the priority, let's take a look at its operation:

  • First we merge our priorities onto the fiber
  • Then determine whether there is another tree (because Raect is a double buffer structure), if so, the content should also be synchronized
  • Then find the parent node of the node and merge this priority into the parent node
  • Then look for the parent node of its parent node, from top to top, until it is synchronized to the root node
function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
    
    
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); // 将传入的lane合并到 fiber 节点上
  let alternate = sourceFiber.alternate; // 另一个fiber树,若是在调用render()初始化时,alternate为null
  if (alternate !== null) {
    
    
    // 若alternate不为空,说明是更新节点,这里将另一棵树也更新优先级
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
  // ... 省略DEV
  // 从父节点到根节点,更新的childLanes字段,表示这个节点的子节点有lane的更新
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    
    
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
    
    
      // 若另一棵fiber树不为空,则同时更新另一棵树的childLanes
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    } else {
    
    
      // alternate为空,说明是mount阶段,不用管
	  // ....
    }
    node = parent;
    parent = parent.return;
  }
  // 当parent为空时,node正常情况下是 HostRoot 类型,返回node节点的stateNode,即FiberRootNode
  if (node.tag === HostRoot) {
    
    
    const root: FiberRoot = node.stateNode;
    return root;
  } else {
    
    
    // 若node不是 HostRoot 类型,可能的情况时,该fiber树是独立于react单独构建出来的
    return null;
  }
}

Then we call markRootUpdatedthe function , which merges the current lane that needs to be updated into the pendingLanes attribute of the fiber root. This attribute, as we mentioned before, represents the lanes waiting to be executed, and then records the event trigger time on the eventTimes attribute.

Where eventTimes is an array of 31 bits in length, corresponding to the 31 bits of lanes, the trigger time of tasks with different priorities will be recorded in different positions of eventTimes

export function markRootUpdated(
  root: FiberRoot,
  updateLane: Lane,
  eventTime: number,
) {
    
    
  root.pendingLanes |= updateLane;
  if (updateLane !== IdleLane) {
    
    
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
  }

  // root.eventTimes和root.lanes两个数组一一对应,-1表示空位,非-1的位置和lane中1的位置相同
  const eventTimes = root.eventTimes;
  const index = laneToIndex(updateLane);
  eventTimes[index] = eventTime;
}

Then we enter ensureRootIsScheduledthe function , which schedules our tasks, let's take a look at what is done in it, it is still the same, let's skip the part that has been explained, and only look at our lanes part:

  • We first determine whether the task that has just been placed in pendingLanes is about to expire (we recorded the trigger time), if there is a task that is about to expire, take it out, set a high priority, and ensure that it is executed as soon as possible. We will expand this part later explain
  • After that, we determine the next batch of lanes. If there are no lanes to execute, it means that the tasks we can execute have been executed, and our task scheduling ends.
  • If there is a task to be executed, we get the one with the highest priority. If the new task has a lower priority than the currently executing task, then we will ignore it and continue rendering. Otherwise, the new task has a higher priority than the currently executing task. If the task is high, then cancel the current task and execute the new task first
  • Finally, according to the priority of this task (synchronous priority or other) to determine what method to call
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
    
    
  const existingCallbackNode = root.callbackNode;

  /**
   * 判断 pendingLanes 中的任务,是否有过期的,
   * 若该任务没有设置过期时间,则根据该任务的lane设置过期时间,
   * 若任务已过期,则将该任务放到 expiredLanes 中,表示马上就要执行,
   * 在后续任务执行中以同步模式执行,避免饥饿问题
   */
  markStarvedLanesAsExpired(root, currentTime);
  // 确定下一条要处理的通道
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
  if (nextLanes === NoLanes) {
    
    
    // 取消任务
    if (existingCallbackNode !== null) {
    
    
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }
  // 获取这批任务中优先级最高的一个
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority;
  if (
    existingCallbackPriority === newCallbackPriority &&
    !(
      __DEV__ &&
      ReactCurrentActQueue.current !== null &&
      existingCallbackNode !== fakeActCallbackNode
    )
  ) {
    
    
    //....
    // 若新任务的优先级与现有任务的优先级一样,则继续正常执行之前的任务
    return;
  }

  // 新任务的优先级大于现有的任务优先级,取消现有的任务的执行
  if (existingCallbackNode != null) {
    
    
    cancelCallback(existingCallbackNode);
  }
    
  // 开始调度任务,判断新任务的优先级是否是同步优先级
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    
    
    if (root.tag === LegacyRoot) {
    
    
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
    
    
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    //.....
    newCallbackNode = null;
  } else {
    
    
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

Let's take a look at each function in detail. markStarvedLanesAsExpiredThe function of the function is to set the expiration time of each lane, find out the expired tasks, increase their priority, and prevent them from starvation:

export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void {
    
    
  const pendingLanes = root.pendingLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  const expirationTimes = root.expirationTimes;

  let lanes = pendingLanes;
  while (lanes > 0) {
    
    
    // 获取当前lanes中最左边1的位置
    const index = pickArbitraryLaneIndex(lanes);
    // 根据位置还原 lane
    const lane = 1 << index;
     
	// 获取当前位置上任务的过期时间
    const expirationTime = expirationTimes[index];
    // 没过期时间,根据优先级添加一个过期时间
    if (expirationTime === NoTimestamp) {
    
    
      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {
    
    
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
    
    
      // 过期了,添加到过期队列中,这个队列的任务需要尽快执行防止饥饿
      root.expiredLanes |= lane;
    }
    // 从lanes中删除lane, 每次循环删除一个,直到lanes等于0
    lanes &= ~lane;
  }
}

getNextLanesWhat the function does is find out the highest priority task we want to execute:

  • First determine whether there are idle tasks, the method is and NonIdleLanesbitwise AND, if the result is not 0, it means that there are no idle tasks, and the lanes of idle tasks are the highest ones, so the priority is the lowest, we have to deal with it last
  • Afterwards, remove the suspended tasks with our suspendedLanes operation, and the suspended tasks cannot be executed until other tasks are completed
  • If there are no unsuspended tasks, we find all pending tasks and execute the pending tasks according to their priority
  • If all non-idle tasks are executed, we execute the idle tasks according to the priority, which is also executed in the order of the unsuspended tasks first and then the suspended tasks
  • If it is rendering, a new task is suddenly added, but this new task has a lower priority than the task being executed, then ignore it and continue rendering. If the priority of the new task is higher than the task being executed, then Then cancel the current task, execute a new task, and return
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
    
    
  // 没任务了,跳出
  const pendingLanes = root.pendingLanes;
  if (pendingLanes === NoLanes) {
    
    
    return NoLanes;
  }
  let nextLanes = NoLanes;

  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  //在将要处理的任务中检查是否有未闲置的任务,如果有的话则需要先执行未闲置的任务
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
  if (nonIdlePendingLanes !== NoLanes) {
    
    
    // 去除挂起的任务
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
    if (nonIdleUnblockedLanes !== NoLanes) {
    
    
      // 有未挂起的任务,获取最高等级的任务
      nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
    } else {
    
    
      // 都挂起了,从挂起的任务中找到优先级最高的来执行
      const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
      if (nonIdlePingedLanes !== NoLanes) {
    
    
        nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
      }
    }
  } else {
    
    
    // 剩下的任务都是闲置的,判断逻辑相同
    const unblockedLanes = pendingLanes & ~suspendedLanes;
    if (unblockedLanes !== NoLanes) {
    
    
      nextLanes = getHighestPriorityLanes(unblockedLanes);
    } else {
    
    
      if (pingedLanes !== NoLanes) {
    
    
        nextLanes = getHighestPriorityLanes(pingedLanes);
      }
    }
  }
  // 从pendingLanes中找不到有任务,则返回一个空
  if (nextLanes === NoLanes) {
    
    
    return NoLanes;
  }
  // wipLanes是正在执行任务的lanes,nextLanes是本次需要执行的任务的lanes,新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,反之,新任务的优先级比正在执行的任务高,那么则取消当前任务,先执行新任务:
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    
    
    const nextLane = getHighestPriorityLane(nextLanes);
    const wipLane = getHighestPriorityLane(wipLanes);
    if (
      nextLane >= wipLane ||
      (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
    ) {
    
    
      return wipLanes;
    }
}

The role of Lane in the Reconciliation stage

Then we enter performConcurrentWorkOnRootthe function to see how lanes work:

  • We call getNextLanesthe method to get the current batch of tasks with the highest priority, because a new task may be generated before the task is executed, and we need to ensure that the executed task is the highest priority at any time.
  • Use shouldTimeSlicethe function to determine what mode to use for rendering. If the task has timed out, we use synchronous mode rendering (because the synchronous mode will not be interrupted, so the problem of hunger is solved), otherwise we use concurrency according to the current priority and rendering mode mode or synchronous mode rendering
function performConcurrentWorkOnRoot(root, didTimeout) {
    
    
  // 再获取一次 lanes
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  if (lanes === NoLanes) 
    return null;
  }

  // shouldTimeSlice 函数根据 lane 的优先级,决定是使用并发模式还是同步模式渲染(解决饥饿问题)
  // didTimeout判断当前任务是否是超时
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes); // React18初始化时走的是这里

  // 省略 .... (失败处理)
  ensureRootIsScheduled(root, now());
  if (root.callbackNode === originalCallbackNode) {
    
    
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
    
    
  // 任务已经过期了,我们使用同步模式渲染
  if ((lanes & root.expiredLanes) !== NoLanes) {
    
    
    return false;
  }
  // 检查当前是否开启并发模式和当前使用的渲染模式是否是并发模式
  if (
    allowConcurrentByDefault &&
    (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
  ) {
    
    
    return true;
  }   
  // 这四个lane都是需要使用同步模式执行的
  const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane;
  // 根据优先级判定使用并发还是同步模式渲染
  return (lanes & SyncDefaultLanes) === NoLanes;
}

Let's go deeper and enter renderRootConcurrentthe method , which calls prepareFreshStackthe function. In this function, we give the priority of the current task passed in workInProgressRootRenderLanesto, subtreeRenderLanes, and workInProgressRootIncludedLanesproperties:

  • **workInProgressRootRenderLanes ** indicates whether there are currently tasks being executed, if there is a value, it means that there are tasks being executed, otherwise, no tasks are being executed.
  • **subtreeRenderLanes ** indicates the collection of lanes of fiber nodes that need to be updated, because we are, when updating fiber nodes later, we will judge whether to update according to this value.
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
    
    
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  workInProgressRoot = root;
    
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;

  return rootWorkInProgress;
}

Then we go all the way to our beginWorkfunction checkScheduledUpdateOrContextto judge whether the previous node can be reused. In this function, if it can be reused, it will not continue to execute. If it cannot be reused, it will use the logic beginWorkof The previous taxonomies deal with each of the different nodes:

function performUnitOfWork(unitOfWork: Fiber): void {
    
    
  // subtreeRenderLanes 标识需要更新的点集合
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
}

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    
    
  //...
  if (current !== null) {
    
    
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // 新旧的 props 是不是相同
    if (
      oldProps !== newProps ||
	  //....
    ) {
    
    
      // 如果不同
      didReceiveUpdate = true;
    } else {
    
    
      // 如果相同
      // checkScheduledUpdateOrContext 函数检查当前 fiber 节点上的 lanes 是否存在 subtreeRenderLanes 中
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
    
    
        didReceiveUpdate = false;
        // 复用之前的节点
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      //....
    }
  } else {
    
    
    didReceiveUpdate = false;
  }
  //...
}
//判断能不能复用
function checkScheduledUpdateOrContext(
  current: Fiber,
  renderLanes: Lanes,
): boolean {
    
    
  const updateLanes = current.lanes;
  // 当前节点的 lanes 是不是在 subtreeRenderLanes 中
  if (includesSomeLane(updateLanes, renderLanes)) {
    
    
    return true;
  }
  if (enableLazyContextPropagation) {
    
    
    const dependencies = current.dependencies;
    if (dependencies !== null && checkIfContextChanged(dependencies)) {
    
    
      return true;
    }
  }
  return false;
}

Afterwards, in our follow-up operations, we will use processUpdateQueueto operate our update queue, which we have mentioned before, which involves the transformation of lanes, let's take a look: this code is mainly for looping out the current fiber node The update object updateQueuein , and then compare the lane mounted on the update object with renderLanes to determine whether the lane on the update object exists renderLaneson , if it exists, it means that the current update object needs to be updated, if it does not exist, the previous lane will be reused state, skip this update object.

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;
      // 判断 updateLane 是否在 renderLanes(subtreeRenderLanes) 上,
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
    
    
        // 若当前 update 的操作的优先级不够。跳过此更新
        //.....
        // 更新优先级
        newLanes = mergeLanes(newLanes, updateLane);
      } else {
    
    
        // 执行更新
      }
    } while (true);
}

The role of Lane in the Commit phase

After the update is complete, let's take a look at the priority operations in the commit phase: for each node, it will directly merge with the lanes that need to be processed in the child to get the lanes that we still need to execute, and remove them pendingLanesfrom This part is the lanes that we have executed. We delete all the executed lanes from our Fiber Root, and then mount the remaining parts that need to be executed back for our next update. Finally, we open our again. The update executes this part of the lane:

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<mixed>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
) {
    
    

  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  // 获取到剩下还需要做更新的lanes
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 清理掉已经更新的 lanes
  markRootFinished(root, remainingLanes);
  remainingLanes = root.pendingLanes;
  if (remainingLanes === NoLanes) {
    
    
    legacyErrorBoundariesThatAlreadyFailed = null;
  }
  // .....
}

export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
    
    
  // 从 pendingLanes 中删除还未执行的lanes,那么就找到了已经执行过的lanes
  const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
  // 将剩下的lanes重新挂载到pendingLanes上,准备下一次的执行
  root.pendingLanes = remainingLanes;

  root.suspendedLanes = 0;
  root.pingedLanes = 0;
  // 从expiredLanes, mutableReadLanes, entangledLanes中删除掉已经执行的lanes
  root.expiredLanes &= remainingLanes;
  root.mutableReadLanes &= remainingLanes;
  root.entangledLanes &= remainingLanes;

  const entanglements = root.entanglements;
  const eventTimes = root.eventTimes;
  const expirationTimes = root.expirationTimes;

  // 取出已经执行的lane,清空它们所有的数据, eventTimes中的事件触发时间,expirationTimes中的任务过期时间等
  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;
  }
}

Summarize

So far our lane processing is finished, let's summarize the whole process:

  • First of all, when we write code to bind events, according to our different events, React will provide him with a lane
  • Later, when we trigger this event to generate an update, we can get the priority of this event, and put the generated update into the update queue corresponding to Fiber
  • After this update is generated, we have changed the corresponding Fiber node and started a new schedule. We need to update our priority from the bottom up. By changing the Fiber and the laneparent node childLanes, these updates will be collected pendingLanesinto , and then update the trigger event of the event into eventTimesthe
  • After that, we start our task scheduling. Each pendingLanestime, whether there is any task that is about to expire. If a task is overdue, put it expiredLanesin the emergency queue and schedule it as soon as possible to prevent it from being hungry. If the task has not set an overdue event, we follow his Lane sets an expiration event for him (the higher the priority, the faster the expiration)
  • Then expiredLaneswe getpendingLanes our current most urgent task according to and and our , judge according to the priority whether the current most urgent task can interrupt the execution of our existing task (high priority preemption), and then judge according to the priority of our task suspencedLanesWhat mode do we use to start our task? We put the batch of tasks with the highest priority calculated subtreeRenderLanesinto , as the execution task of this scheduling
  • Then we perform a DIFF operation. During the operation, we judge whether it can be directly reused according to whether the lanes on the corresponding Fiber are subtreeRenderLanesin
  • If it cannot be reused, we call back processUpdateQueueto operate the update queue on our fiber, and we subtreeRenderLanesjudge whether the update can be started according to whether the lanes on the fiber are running. If a task is updated, we delete the corresponding lane
  • After the DIFF process is over, we enter the commit stage, we merge the lanes of each node and child nodes, calculate our lanes that have not been updated and the lanes that have been updated, delete the lanes that have been updated from the root, and put the lanes that have not yet been updated The updated lanes are used as our next scheduled task, and then a new schedule is started, and the cycle repeats until our tasks are all run

The above is the role played by the lane model in our entire React update rendering process. After this article, we have a detailed understanding of the entire React update rendering from update task generation to scheduling to how to implement update rendering. After that, we Come back and look at some advanced features. The next article may start with Hooks, so stay tuned!

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/130402847