Why do YOU need priority

The ultimate purpose of priority mechanism is to realize priority execution of high-priority tasks and delay execution of low-priority tasks.

The essence of this is to interrupt low-priority tasks when higher-priority tasks come in.

React runtime in synchronous mode

We know that in synchronous mode, the entire process from setState to virtual DOM traversal to real DOM updates is performed synchronously and cannot be interrupted, which can cause a problem — updates triggered by user events are blocked.

What are user event-triggered updates blocked? If React is performing an update task, the user triggers an interaction event and setState is executed in the event callback. In synchronous mode, the update task will wait until the current update task completes. If the React update task takes a long time, the update task triggered by a user event cannot be executed in a timely manner. As a result, the next update task is blocked.

In this case, we want to be able to respond to user-triggered events in a timely manner and perform user-triggered update tasks first, which is called asynchronous mode

We can compare the update task execution in synchronous mode with that in asynchronous mode (priority mechanism)

import React from "react";
import "./styles.css";

export default class extends React.Component {
  constructor() {
    super(a);this.state = {
      list: new Array(10000).fill(1),};this.domRef = null;
  }
  componentDidMount() {
    setTimeout(() = > {
      console.log("SetTimeout ready to update", performance.now());
      this.setState(
        {
          list: new Array(10000).fill(Math.random() * 10000),
          updateLanes: 16
        },
        () = > {
          console.log("SetTimeout update completed", performance.now()); }); },100);
    setTimeout(() = > {
      this.domRef.click();
    }, 150);
  }

  render() {
    const { list } = this.state;
    return (
      <div
        ref={(v)= >(this.domref = v)} className="App" onClick={() => {console.log("click ready to update ", performance.now()); this.setState( { list: new Array(10000).fill(2), updateLanes: 1}, () => {console.log("click update completed ", performing.now ()); }); }} > {list.map((i, index) => (<h1 key={i + +index} >Hello {i}</h1>
        ))}
      </div>); }}Copy the code

The above code prints in synchronous mode

You can see that the update triggered by the Click event will wait for the update triggered by setTimeout to be executed

The above code prints in asynchronous mode

As you can see, updates triggered by the Click event take precedence over updates triggered by setTimeout, so that the user event is timely responded to and the setTimeout update task (low priority task) is interrupted.

Above code I wrote a demo in Codesandbox, respectively running in react15 and React18 two versions, you can be directly interested in the up to play

How to optimize the React runtime with priority mechanism

To address the drawbacks of synchronous rendering, we wanted to make the following optimizations for React

  1. Prioritize the updates that are triggered in different scenarios so that we can decide which tasks to prioritize
  2. If a higher-priority task comes in, we need to interrupt the current task and execute the higher-priority task
  3. Ensure that low priority tasks are not constantly interrupted and can be upgraded to the highest priority tasks after a certain period of time

Determine scheduling priorities in different scenarios

React source code: there are so many priority-related words. How do you tell them apart?

In fact, React is mainly divided into two priorities: Scheduler priority and Lane priority. The Event priority is derived from lane priority

  • Lane priority: checks the priorities of ongoing and scheduled tasks before task scheduling to determine whether ongoing tasks need to be interrupted
  • Event priority: Essentially lane priority, lane priority is universal, event priority is more combined with browser native events, lane priority classification and mapping
  • Scheduler priority: Used primarily to calculate the expiration time of tasks in time sharding

Lane priority

Lane priority can be understood by the concept of race track. There are 31 lane priorities, which can be represented by the binary value of 31 bits. Each value represents a lane priority corresponding to a race track

priority Decimal value Binary values The track position
NoLane 0 0000000000000000000000000000000 0
SyncLane 1 0000000000000000000000000000001 0
InputContinuousHydrationLane 2 0000000000000000000000000000010 1
InputContinuousLane 4 0000000000000000000000000000100 2
DefaultHydrationLane 8 0000000000000000000000000001000 3
DefaultLane 16 0000000000000000000000000010000 4
TransitionHydrationLane 32 0000000000000000000000000100000 5
TransitionLane1 64 0000000000000000000000001000000 6
TransitionLane2 128 0000000000000000000000010000000 7
TransitionLane3 256 0000000000000000000000100000000 8
TransitionLane4 512 0000000000000000000001000000000 9
TransitionLane5 1024 0000000000000000000010000000000 10
TransitionLane 2048 0000000000000000000100000000000 11
TransitionLane7 4096 0000000000000000001000000000000 12
TransitionLane8 8192 0000000000000000010000000000000 13
TransitionLane9 16384 0000000000000000100000000000000 14
TransitionLane10 32768 0000000000000001000000000000000 15
TransitionLane11 65536 0000000000000010000000000000000 16
TransitionLane12 131072 0000000000000100000000000000000 17
TransitionLane13 262144 0000000000001000000000000000000 18
TransitionLane14 524288 0000000000010000000000000000000 19
TransitionLane15 1048576 0000000000100000000000000000000 20
TransitionLane16 2097152 0000000001000000000000000000000 21
RetryLane1 4194304 0000000010000000000000000000000 22
RetryLane2 8388608 0000000100000000000000000000000 23
RetryLane3 16777216 0000001000000000000000000000000 24
RetryLane4 33554432 0000010000000000000000000000000 25
RetryLane5 67108864 0000100000000000000000000000000 26
SelectiveHydrationLane 134217728 0001000000000000000000000000000 27
IdleHydrationLane 268435456 0010000000000000000000000000000 28
IdleLane 536870912 0100000000000000000000000000000 29
OffscreenLane 1073741824 1000000000000000000000000000000 30

The event priority

EventPriority Lane The numerical
DiscreteEventPriority Discrete events. Click, keyDown, FocusIn, etc., events are not triggered consecutively, so quick response can be achieved SyncLane 1
ContinuousEventPriority Continuous events. Events such as drag, scroll, mouseover, etc., are triggered continuously. Fast response may block rendering and have a lower priority than discrete events InputContinuousLane 4
DefaultEventPriority Default event priority DefaultLane 16
IdleEventPriority Priority of idle IdleLane 536870912

The scheduler priority

SchedulerPriority EventPriority More than > 17.0.2 < > 17.0.2
ImmediatePriority DiscreteEventPriority 1 99
UserblockingPriority Userblocking 2 98
NormalPriority DefaultEventPriority 3 97
LowPriority DefaultEventPriority 4 96
IdlePriority IdleEventPriority 5 95
NoPriority 0 90

Conversion between priorities

  • Switch from Lane priority to event priority (see lanesToEventPriority)

    • Conversion rule: Return the event priority of the incoming lane in the form of an interval. For example, if the priority passed is not greater than Discrete, it returns Discrete, and so on
  • Scheduler priority (see ensureRootIsScheduled function)

    • Transition rules: Refer to the Scheduler priority table above
  • Change the event priority to lane priority (see getEventPriority)

    • Conversion rules: Non-discrete and continuous events will be converted according to certain rules. For details, please refer to the event priority table above

How is the priority mechanism designed

As priority mechanism, we may soon can associate to the priority queue, its most prominent feature is the highest priority, first out, the react of priority mechanism is similar to the priority queue, but the use of the concept of the track, cooperate with operation abounded the function of the queue, compared with the priority queue, reading and writing is faster, more easy to understand

Design ideas

  1. Merge tracks: Maintains a queue that can store occupied tracks
  2. Release track: Release track to be occupied according to priority
  3. Find the highest priority track: Gets the highest priority track in the queue
  4. Quick Location track index: Gets the track position in the queue based on priority
  5. Determine whether the track is occupied: Determine whether the track where the priority resides is occupied according to the incoming priority

Merge the track

  • scenario
    • For example, the priority of the task currently being scheduled is DefaultLane, and the user clicks to trigger the update, a high-priority task SyncLane is generated, and the track occupied by this task needs to be stored
  • The operation process
    • Operation mode: bit or operation –a | b
    • Result: DefaultLane and SyncLane occupy track 1 and 5 respectively
DefaultLane priority for 16, SyncLane priority 1 the binary value of 16 | 1 = 17, 17, 10001 the binary value of 16, 10000, 1 binary value is 00001Copy the code

Release of the circuit

  • scenario
    • SyncLane mission completed, need to release occupied track
  • The operation process
    • Operation mode: bit and + bit non –a & ~b
    • Result: The SyncLane track is freed, leaving only DefaultLane
17 & ~1 = 16 The binary value of 17 is 10001. The binary value of 2 is 00010. The sign bit of -2 is reversed to 10010. The sum of 10001 and 10010 gives 10000, which is 16 in decimalCopy the code

Find the highest priority track

  • scenario
    • There are two “DefaultLane” and “SyncLane” missions with priority to the track, and after entering the ensureRootIsScheduled method I need to schedule the one with the highest priority first, so I need to find the one with the highest priority
  • The operation process
    • Operation mode: bit and + sign bit inverse –a & -b
    • Result: SyncLane task with the highest priority is found. SyncLane task is a synchronization task, and Scheduler will schedule the current application root node based on the synchronization priority
17 &-17 = 1 The binary value of 17 is 10001. The binary value of 17 is 00001. The sum of 10001 and 00001 gives 1, which is SyncLaneCopy the code

Quick location track index

  • scenario
    • Hunger task wake up: before launching the dispatch, we need to make a judgment on all the tracks in the queue to determine whether the task of the track is expired. If it is, the expired task will be preferentially executed. To do this, we need to maintain an array with a length of 31, and the subscript index of each element of the array corresponds to the 31 priority tracks one by one. The array stores the expiration time of the task, and we hope to quickly find the corresponding position of the priority in the array according to the priority.
  • The operation process
    • Math. Clz32
    • If DefaultLane is set to index 4, you can set eventTimes and expirationTimes on the root node to -1, and then execute the expiration task
// Find DefaultLane track index 31 - math.clz32 (16) = 4Copy the code
  • What is Math. Clz32 used for?
    • Gets the number of leading zeros in a binary value corresponding to a decimal number.
    • So let’s subtract from 31Math.clz32To get the index of the track

Determine if the track is occupied

Refer to the code above in CodesandBox. In asynchronous mode, there will be cases where high-priority tasks jump the queue, and the calculation of state in this case will be somewhat different from that in synchronous mode.

  • scenario
    1. We’re not going to update our setState immediatelystateInstead, one will be generated based on the contents of setStateUpdateObject that contains properties such as update content, update priority, and so on.
    2. updatestateThis action is in theprocessUpdateQueueIt’s going to be evaluated in the functionUpdateTo determine whether to evaluate this in this round of tasks if the priority track of the object is occupiedUpdateThe object’sstate
      • If occupied, representsUpdateThe priority of the object is equal to the task currently in progressUpdateObject to calculatestateAnd update to the Fiber nodememoizedStateOn the properties
      • If not occupied, the priority of the current ongoing task is higher than thisUpdateObject of high priority, corresponding to the lower priorityUpdateThe state of the object is not calculated until the next low-priority task is restarted
  • The operation process
    • Operation mode: bit and(renderLanes & updateLanes) == updateLanes
    • Result: 0 indicates that the current scheduling priority is higher than that of an Update object
Formula (1 & 16) == 16 the binary value of 1 is 00001 and the binary value of 16 is 10000Copy the code

How to integrate prioritization into the React runtime

Generate an update task

The process of generating a task is actually very simple. The entry point is the setState function, which we often use

Inside the setState function is the enqueueUpdate function. EnqueueUpdate does its job in four steps:

  1. Gets the priority of this update.
  2. createUpdateobject
  3. Associate this update priority to the current Fiber node, parent node, and application root node
  4. initiateensureRootIsScheduledScheduling.

Step 1: Obtain the priority of the update

The job of step 1 is to call requestUpdateLane to get the priority of the update task

  1. If the current is notconcurrentmodel
    • Currently not in render phase. Return syncLane
    • Currently in render phase. Return the highest priority in workInProgressRootRenderLanes (here is using the above priority operation mechanism, find out the highest priority track)
  2. If the current isconcurrentmodel
    • If you need to perform a delayed task, for exampleSuspend,useTransition,useDefferedValueFeatures. intransitionSearch for free tracks in the priority of type.transitionThere are 16 types of tracks, from track 1 to track 16, when reaching track 16 after the nexttransitionType of missions will return to track 1 and so on.
    • performgetCurrentUpdatePriorityFunction. Gets the current update priority. If not forNoLaneIt returns
    • performgetCurrentEventPriorityFunction. Returns the current event priority. Returns if no event is generatedDefaultEventPriority

In general, the order of priority selection for requestUpdateLane is as follows:

SyncLane  >>  TransitionLane  >>  UpdateLane  >>  EventLane
Copy the code

I think a lot of you might be confused by the question, why are there so many functions that get priority

Step 2: Create an Update object

There’s not a lot of code here, just wrapping the setState argument in an object for the Render phase to use

function createUpdate(eventTime, lane{
  var update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payloadnull.callbacknull.nextnull
  };
  return update;
}
Copy the code

Step 3: Associate priorities

Two concepts are explained here, HostRoot and FiberRootNode

  • HostRootIs:ReactDOM.renderIs the root node of the component tree.HostRootThere could be more than one becauseReactDOM.renderCan be called multiple times
  • FiberRootNode: The React root node. Each page has only one React root node. Available from theHostRootThe node’sstateNodeProperty access

Here the correlation priority performs two main functions

  1. markUpdateLaneFromFiberToRoot. This function does two main things
    • Merges the priority into the Lanes property of the current Fiber node
    • Merge the priority into the parent node’s childLanes property (tells the parent how many tracks his child node has to run)

    But because the function passed in the Fiber node isHostRoot, that is,ReactDOM.renderThat is, there is no parent node, so the second thing is not done

  2. markRootUpdated. This function also does two main things
    • Merge the priority of tasks to be scheduled to the react root node
    • Calculate the eventTime occupied by the current task priority track

The React priority mechanism does not run independently on each component node. Instead, it relies on a global React root node to control task scheduling in the following component tree

What is the use of associating priorities with these Fiber nodes?

Let’s start with the differences

  • Lanes: Only exist on non-React root nodes. Record lane priorities of the current Fiber node
  • ChildLanes: Only exist on the root node of non-React applications. Record the lane priorities of all sub-fiber nodes under the current Fiber node
  • PendingLanes: Records all lanes on the root node of the React applicationHostRootIs the Lane priority of

Application Scenarios

  1. Release the track. The priority mechanism mentioned above mentioned that the track will be freed after the task is completed, specifically after the commit phase is completed, i.emarkRootFinishedFunction.
  2. Determine if the track is occupied. In the render phasebeginWorkThere’s a lot of judgment in the processchildLanesDetermination of occupancy

Step 4: Initiate scheduling

The key ensureRootIsScheduled step is the call to the function “ensureRootIsScheduled”, whose logic is one of two ensureRootIsScheduled features: high priority tasks interrupt low priority tasks and starvation tasks

High-priority tasks interrupt low-priority tasks

This part of the process can be divided into three parts

  1. cancelCallback
  2. pop(taskQueue)
  3. The low-priority task is restarted

cancelCallback

var existingCallbackNode = root.callbackNode;
var existingCallbackPriority = root.callbackPriority;
var newCallbackPriority = getHighestPriorityLane(nextLanes);
if (existingCallbackPriority === newCallbackPriority) {
    ...
    return;
}
if(existingCallbackNode ! =null) {
    cancelCallback(existingCallbackNode);
}

newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
Copy the code

The above are some snippets of ensureRootIsScheduled functions, with the variables explained first

  • existingCallbackNode: Ongoing task in the current Render phase
  • existingCallbackPriority: Priority of ongoing tasks in the current Render phase
  • newCallbackPriority: Indicates the scheduling priority

This determines whether the existingCallbackPriority and newCallbackPriority priorities are equal, and if so, the update is merged into the current ongoing task. If they are not equal, the update task has a higher priority and the ongoing task needs to be interrupted

How do I interrupt a task?

  1. The key functioncancelCallback(existingCallbackNode).cancelCallbackThe function is to takeroot.callbackNodeAssign a value to null
  2. performConcurrentWorkOnRootThe function is going to takeroot.callbackNodeIt’s cached, and it’s evaluated at the end of the functionroot.callbackNodeIs it the same as the value cached at the beginning? If not, it representsroot.callbackNodeThe value is null. Higher priority tasks are coming in.
  3. At this timeperformConcurrentWorkOnRootThe return value is null

Below is performConcurrentWorkOnRoot code snippet

. var originalCallbackNode = root.callbackNode; . / / function at the end of the if (root callbackNode = = = originalCallbackNode) {return performConcurrentWorkOnRoot. Bind (null, root); } return null;Copy the code

EnsureRootIsScheduled code fragment above, can know performConcurrentWorkOnRoot function were scheduleCallback scheduling function, specific return after the logic of the need to find the Scheduler module

pop(taskQueue)

var callback = currentTask.callback;
if (typeof callback === 'function') {
  ...
} else {
  pop(taskQueue);
}
Copy the code

The code snippet above is the Scheduler module workLoop function, currentTask. The callback is scheduleCallback the second parameter, namely performConcurrentWorkOnRoot function

Undertake on a theme, if performConcurrentWorkOnRoot function returns null, workLoop internal pop (taskQueue) should be executed, the current task from taskQueue pop-up.

The low-priority task is restarted

The previous step said that a low-priority task was ejected from the taskQueue. How do I restart a low-priority task after the high-priority task is finished?

The key is in the commitRootImpl function

varremainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); markRootFinished(root, remainingLanes); . ensureRootIsScheduled(root, now());Copy the code

“MarkRootFinished” is one of the ensureRootIsScheduled functions that can be used to release tracks held by completed tasks. That is, uncompleted tasks can still be held, so a new schedule can be called again to restart low-priority tasks. We can look at the restart judgment

var nextLanes = getNextLanes(
    root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
// If nextLanes is NoLanes, all tasks are completed
if (nextLanes === NoLanes) {
    ...
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    // If nextLanes is NoLanes, we can end scheduling
    return;
}
// If nextLanes are not NoLanes, there are unfinished tasks, i.e. low-priority tasks that have been interrupted.Copy the code

Hunger mission problem

As stated above, low-priority tasks are restarted after high-priority tasks finish executing, but suppose that if high-priority tasks continue to come in, my low-priority tasks will never restart.

So the react to handle solve the problem of hunger task, the react at the beginning of the ensureRootIsScheduled function markStarvedLanesAsExpired function to do the following treatment: (reference)

var lanes = pendingLanes;
while (lanes > 0) {
    var index = pickArbitraryLaneIndex(lanes);
    var lane = 1 << index;
    var 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;
}
Copy the code
  1. Through 31 tracks, judge whether the expiration time of each track isNoTimestamp, if yes, and there are tasks waiting to be executed on the track, initialize the expiration time for the track
  2. If the track has an expiration time and the expiration time is less than the current time, it indicates that the task has expired and the current priority needs to be merged toexpiredLanes, so that the next render phase schedules the current with synchronous priorityHostRoot

You can refer to a function to be executed by the render phase of performConcurrentWorkOnRoot code snippet

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

You can see that as soon as shouldTimeSlice returns false, renderRootSync will be executed, which is the render phase with the synchronization priority. ShouldTimeSlice is the same logic as expiredLanes

function shouldTimeSlice(root, lanes{
  // If there is something in expiredLanes, there is a hunger quest
  if((lanes & root.expiredLanes) ! == NoLanes) {return false;
  }
  var SyncDefaultLanes = InputContinuousHydrationLane | 
                          InputContinuousLane | 
                          DefaultHydrationLane | 
                          DefaultLane;

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

conclusion

The React priority mechanism is not an independent, decoupled module in the source code, but involves all aspects of the operation of React. Finally, the use of priority mechanism in the source code is reviewed to give people a better understanding of the priority mechanism.

  1. Time fragmentation. It involves task interruption and sharding duration calculation according to priority
  2. SetState generates the Update object. Each Update object has a Lane attribute that represents the priority of the Update
  3. High-priority tasks interrupt low-priority tasks. Each scheduling task compares the highest priority of the ongoing task with that of the current task. If the task is not equal, it indicates that the current task has a higher priority and needs to be interrupted.
  4. The low-priority task is restarted. coordinate(reconcile)The next stage is rendering(renderer)At the end of the commit phase, a call is calledensureRootIsScheduledInitiate a new schedule to execute pending low-priority tasks.
  5. Hunger quest awaken. At the beginning of each schedule, a check is made for expired tasks, and if so, the next render task is performed with synchronous priority(reconcile), the synchronization priority is the highest priority and will not be interrupted

Finally, an overall React mind map is sorted out, and the Scheduler module is basically sorted out. The following lectures will focus on the Render stage and commit stage.