During my study of the React source code, the concept of priority levels is one of the areas where I invested significant time, as I believe it’s one of the most crucial components for understanding React thoroughly. With the groundwork laid in the previous two chapters, we can now delve into priority levels in more detail. This article will use a progressive approach to unpack the code, explaining why different priority levels exist, the reasons for their distinctions, and how they operate within React’s execution flow. Let’s get started!
Key Questions to Consider
Before we dive in, it’s helpful to keep these questions in mind, as this article primarily addresses them:
Why separate event priority from update priority?
How does React’s Lane model work?
What is the structure of
updateQueue
on a fiber, and what are its advantages?Additional insights on various related topics.
Update Priority Levels
The Lane Model
Readers of this series may already be somewhat familiar with React and know that it uses the Lane model to represent priority levels. However, for completeness, let’s introduce the Lane model, which more experienced readers can skip.
Think of a “lane” as akin to a track on a racetrack, where some tracks are reserved for high-speed racing while others are for slower practice sessions. In React, different update tasks are assigned to different lanes, each with its priority level. To optimize calculations, React uses a 31-bit bitmap to represent these priority levels, allowing up to 31 different levels, as shown below:
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; // 1 export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010; // 2 export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100; // 4 export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000; //16 export const DefaultLane: Lane = /* */ 0b0000000000000000000000000010000; //32 // ...
Using the Lane model, React can represent various combinations of priorities with a single value, quickly isolating the most urgent priority level and optimizing performance through bitwise operations. According to the Lane model definition above, the priority levels are:
Sync Priority (SyncLanes)
Continuous Priority (InputContinuousLanes)
Default Priority (DefaultLanes)
Transition Priority (TransitionLanes)
Retry Priority (RetryLanes)
Idle Priority (IdleLanes)
Offscreen Priority (OffscreenLanes)
A Quick Recap
In the article "In-depth Understanding of React: Initialization Process," we analyzed the process from createRoot
to updateContainer
in React. We learned about event priority, which involves classifying all events and assigning each a priority level based on its characteristics. React defines four event priorities:
var DiscreteEventPriority = SyncLane; // Discrete events var ContinuousEventPriority = InputContinuousLane; // Continuous events var DefaultEventPriority = DefaultLane; // Default events var IdleEventPriority = IdleLane; // Idle events
Each event priority corresponds to a lane, facilitating conversion between event priority and the lane’s priority. Now, let’s continue from our previous exploration of updateContainer
.
function updateContainer(element, container, parentComponent, callback) { // element is <App/>, container is #root, parentComponent is null, callback is null var current$1 = container.current; // The first fiber node, RootFiber var eventTime = requestEventTime(); // Time since the page opened, not epoch time var lane = requestUpdateLane(current$1); // Retrieve the update priority level ... var update = createUpdate(eventTime, lane); // Create an update update.payload = { element: element }; callback = callback === undefined ? null : callback; update.callback = callback; var root = enqueueUpdate(current$1, update, lane); // Add the update to the fiber tree if (root !== null) { scheduleUpdateOnFiber(root, current$1, lane, eventTime); // Start scheduling the update } return lane; }
If we categorize React’s UI rendering, there are two main types: mounting and updating. updateContainer
is the primary function used in both processes. It performs the following tasks:
Retrieves the update priority level.
Creates an
update
object.Adds the
update
object to the fiber’s update queue.Begins scheduling the update.
Let’s explore the requestUpdateLane
function, which determines the current update priority level in React. When APIs like render
, setState
, setXXX
, and forceUpdate
are called, requestUpdateLane
fetches the update priority:
function requestUpdateLane(fiber) { var mode = fiber.mode; // 1. Synchronous if ((mode & ConcurrentMode) === NoMode) { return SyncLane; } // 2. Transition var isTransition = requestCurrentTransition() !== NoTransition; if (isTransition) { ... return currentEventTransitionLane; } // 3. Update Type var updateLane = getCurrentUpdatePriority(); if (updateLane !== NoLane) { return updateLane; } // 4. Event var eventLane = getCurrentEventPriority(); return eventLane; }
When an API related to updates is called, React first identifies whether the mode supports concurrent updates. If not, it defaults to synchronous priority. If it’s a transition, it returns the corresponding transition priority.
For example:
const App = () => { const [num , setNum] = useState(0) const onClick = () => setNum(num + 1) return <button onClick={onClick}>{num}</button> }
When you click the button, the global currentUpdatePriority
is set to the appropriate event priority during onClick
, ensuring an accurate update priority level. Some updates, like those triggered by IO or async operations, don’t have a related event and use DefaultEventPriority
:
const App = ()=> { const [num , setNum] = useState(0) useEffect(()=>{ fetchData().then(res => setNum(res)) }, []) }
Here, setNum()
gets a DefaultLane
update priority. You may wonder, why distinguish between event priority and update priority? This distinction is crucial because one event can trigger updates with different priority levels:
const App = () => { const [num, setNum] = useState(0); const [count, setCount] = useState(0); return ( <div> <h1 id="h1">{num}</h1> <button onClick={() => { setNum(num + 1); // SyncLane startTransition(() => { setCount(count + 1); // TransitionLane }); }}> {count} </button> </div> ); };
By handling priorities distinctly for events and updates, React enables more nuanced control over complex interactions, resulting in a smoother, more efficient rendering experience.
3. Update Queue
Based on our analysis of updateContainer
, let’s now look at the process of creating the queue. The implementation of createUpdate
is as follows:
var UpdateState = 0; var ReplaceState = 1; var ForceUpdate = 2; var CaptureUpdate = 3; function createUpdate(eventTime, lane) { var update = { eventTime: eventTime, lane: lane, tag: UpdateState, // 0 payload: null, callback: null, next: null, // Seeing this, we can tell it’s a linked list }; return update; }
Creating an update
object is straightforward, holding the priority level and timestamp of the current update, as well as the update's content (payload
), which here is the <App />
component.
Next is enqueueUpdate
, which focuses on adding updates to the fiber
's update queue. This is a key area:
function enqueueUpdate(fiber, update, lane) { var updateQueue = fiber.updateQueue; if (updateQueue === null) { return null; } var sharedQueue = updateQueue.shared; ... enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane); }
For the root fiber
node we created earlier, its updateQueue
is structured as follows:
function enqueueConcurrentClassUpdate(fiber, queue, update, lane) { var interleaved = queue.interleaved; // null if (interleaved === null) { update.next = update; // Indicates this is the first item ... } else { update.next = interleaved.next; interleaved.next = update; } queue.interleaved = update; return markUpdateLaneFromFiberToRoot(fiber, lane); }
The basic logic here is: if interleaved
currently exists, place the new update
object at the head of the linked list, setting the head of the previous list as the next node in the current update, thereby forming a circular linked list. If interleaved
doesn’t exist, it forms a circular linked list with a single item.
Why design updateQueue
as a circular linked list, and one that points to the tail of the list? The primary purpose is to enhance performance. In React’s execution flow, new update objects frequently need to be added to the end of the list. In traditional linked list implementations, each addition to the end of the list requires traversing it entirely. With this structure, we can access interleaved
as the last node in the list directly, while the first node is easily located since the last node’s next pointer leads to it. Once this step is complete, the next step is marking via markUpdateLaneFromFiberToRoot
.
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) { sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); var alternate = sourceFiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, lane); } var node = sourceFiber; var parent = sourceFiber.return; while (parent !== null) { parent.childLanes = mergeLanes(parent.childLanes, lane); alternate = parent.alternate; if (alternate !== null) { alternate.childLanes = mergeLanes(alternate.childLanes, lane); } node = parent; parent = parent.return; } if (node.tag === HostRoot) { var root = node.stateNode; return root; } else { return null; } }
The primary function here is marking. Each fiber node has two attributes, lanes
and childLanes
, representing the current priority level set by itself and its subtree’s accumulated update priority levels. Imagine a fiber tree:
Assume at a particular moment, Son1
triggers an update with priority level 4, and other nodes have priority levels set to 0. After marking, the result would look like this:
Eventually, FiberRoot
is returned. The main benefit of this approach lies in optimizing task execution during subsequent operations, like building a new fiber tree. Since the process begins from the root node, React can skip nodes as it follows childLanes
that don’t match the renderLanes
of the current update. In this example, Son2
would be skipped, reducing unnecessary fiber node creation.
This completes the addition of the task to the update queue. Next, we move on to the actual scheduling of updates. Due to length constraints, the next article will introduce a new priority level: Task Priority.