In the React Fiber architecture, the task queue is the core component that implements interruptible rendering and priority scheduling. This article mainly talks about Task Queue, Interruptible Rendering, Scheduling and Synchronization Mode of Fiber.
After understanding this entire section, you have almost understood the most important things and main lines in Fiber, and the rest are scattered small details.
2. Concept and function of task queue
Before diving into the source code, we need to first understand the importance of task queues in the React Fiber architecture.
2.1 What is a task queue?
A task queue is a data structure used to store and manage tasks that need to be executed. In React Fiber, the task queue is mainly used to manage various operations during the update and rendering process, such as component updates, DOM operations, etc.
2.2 The role of task queue
1. Task Scheduling: Sort and schedule tasks according to priority.
2. Interruption and Resume: Supports task interruption and recovery, enabling interruptible rendering.
3. Priority Management: Assign different priorities to different types of updates.
4. Performance Optimization: Improve rendering performance and user experience through reasonable scheduling of tasks.
3. Task queue implementation in React source code
First, let’s look at how task queues are implemented.
3.1 Data structure
React uses Min Heap to implement task queues. Minimax heap is a special kind of binary tree in which the value of each node is less than or equal to the value of its child node. This structure is very suitable for priority queue implementation because it can complete insertion and deletion operations in O(log n) time complexity.
File: packages/scheduler/src/Scheduler.js
var taskQueue = []
var timerQueue = []
// ...other code
function push(heap, node) {
var index = heap.length
heap.push(node)
siftUp(heap, node, index)
}
function peek(heap) {
return heap.length === 0 ? null : heap[0]
}
function pop(heap) {
if (heap. length === 0) {
return null
}
var first = heap[0]
var last = heap.pop()
if (last !== first) {
heap[0] = last
siftDown(heap, last, 0)
}
return first
}
```
In this code, we can see the definitions of task queue (`taskQueue`) and timer queue (`timerQueue`), as well as the basic functions for operating the heap: `push`, `peek` and `pop`.
### 3.2 Representation of tasks
In React, each task is represented as an object with the following main properties:
**File: packages/scheduler/src/Scheduler.js**
```javascript
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1
}
```
* `id`: The unique identifier of the task.
* `callback`: The callback function of the task.
* `priorityLevel`: The priority of the task.
* `startTime`: The start time of the task.
* `expirationTime`: The expiration time of the task.
* `sortIndex`: Index used for sorting in the queue.
### 3.3 Definition of priority
React defines multiple priority levels to distinguish between different types of tasks:
**File: packages/scheduler/src/SchedulerPriorities.js**
```javascript
export const NoPriority = 0
export const ImmediatePriority = 1
export const UserBlockingPriority = 2
export const NormalPriority = 3
export const LowPriority = 4
export const IdlePriority = 5
```
These priorities are arranged from high to low and are used to determine the order in which tasks are executed.
## 4\. Task queue creation process
The creation of task queues is the foundation of the React Fiber architecture.
### 4.1 Initialization
The task queue is created when React is initialized. In the `Scheduler.js` file, we can see the initialization of the queue:
```javascript
var taskQueue = []
var timerQueue = []
```
Two queues are created here:
* `taskQueue` is used to store tasks to be executed
* `timerQueue` is used to store delayed execution tasks
### 4.2 Add task
When a new task needs to be added, React will call the `unstable_scheduleCallback` function:
**File: packages/scheduler/src/Scheduler.js**
```javascript
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime()
var startTime
if (typeof options === 'object' && options !== null) {
var delay = options.delay
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay
} else {
startTime = currentTime
}
} else {
startTime = currentTime
}
var timeout
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT
break
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT
break
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT
break
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT
break
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT
break
}
var expirationTime = startTime + timeout
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime
push(timerQueue, newTask)
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout()
} else {
isHostTimeoutScheduled = true
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime)
}
} else {
newTask.sortIndex = expirationTime
push(taskQueue, newTask)
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true
requestHostCallback(flushWork)
}
}
return newTask
}
```
This function completes the following key steps:
1. Calculate the start time and expiration time of the task.
2. Set the timeout based on priority.
3. Create a new task object.
4. Add the task to the appropriate queue (`taskQueue` or `timerQueue`).
5. If necessary, schedule a host callback or timeout.
### 4.3 Task sorting
Tasks are ordered as they are added to the queue. The key to sorting is the `sortIndex` attribute:
* For delayed tasks (`timerQueue`), `sortIndex` is the start time of the task.
* For tasks that are executed immediately (`taskQueue`), `sortIndex` is the expiration time of the task.
The sorting process is implemented through the `push` function, which internally calls `siftUp` to maintain the properties of the small top heap:
**File: packages/scheduler/src/Scheduler.js**
```javascript
function push(heap, node) {
var index = heap.length
heap.push(node)
siftUp(heap, node, index)
}
function siftUp(heap, node, i) {
var index = i
while (true) {
var parentIndex = (index - 1) >>> 1
var parent = heap[parentIndex]
if (parent !== undefined && compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node
heap[index] = parent
index = parentIndex
} else {
// The parent is smaller. Exit.
return
}
}
}
```
This process ensures that the tasks in the queue are always arranged in the correct order, so that the highest priority task is always at the top of the queue.
## 5\. Scheduling process of task queue
After creating the task queue, React needs to schedule these tasks efficiently.
### 5.1 Scheduler initialization
React uses a scheduling system to manage the execution of tasks. The core of this system is several key functions and mechanisms in Scheduler.js.
1. Scheduler initialization:
When we call `requestHostCallback`, it sets `scheduledHostCallback` and sends a message through `MessageChannel`:
**File: packages/scheduler/src/Scheduler.js**
```javascript
//Create a MessageChannel for task scheduling
const channel = new MessageChannel()
const port = channel.port2
//Request host callback function
function requestHostCallback(callback) {
//Set the scheduling callback function
scheduledHostCallback = callback
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
//Send a message through MessageChannel to trigger task execution
port.postMessage(null)
}
}
//Message event handling function
channel.port1.onmessage = performWorkUntilDeadline
```
Here's how this code works:
* Create a `MessageChannel` for communication between the main thread and the scheduler.
* The `requestHostCallback` function is used to set the callback function to be executed.
* When a new callback function is set, if the message loop is not already running, send a message to start it.
* `channel.port1.onmessage` is set to the `performWorkUntilDeadline` function, which will be called when a message is received.
2. Execute scheduling tasks:
The `performWorkUntilDeadline` function is the core of the scheduler, which is responsible for executing tasks and managing time:
**File: packages/scheduler/src/Scheduler.js**
```javascript
let scheduledHostCallback = null
let isMessageLoopRunning = false
let startTime = -1
function performWorkUntilDeadline() {
if (scheduledHostCallback !== null) {
// Get the current time
const currentTime = getCurrentTime()
// Calculate deadline
deadline = currentTime + yieldInterval
//Mark start time
const hasTimeRemaining = true
try {
// Execute the scheduled callback function
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime)
if (!hasMoreWork) {
// If there is no more work, stop the message loop
isMessageLoopRunning = false
scheduledHostCallback = null
} else {
// If there is more work, continue sending messages
port.postMessage(null)
}
} catch (error) {
// If an error occurs, continue sending messages and throw an error
port.postMessage(null)
throw error
}
} else {
isMessageLoopRunning = false
}
//Reset start time
startTime = -1
}
```
The workflow of this function is as follows:
* Check if there is a scheduled callback function.
* If there is, execute this callback function and pass in the time information.
* Determine whether to continue looping based on the return value of the callback function.
* If an error occurs, make sure to keep sending messages to keep the loop going, then throw the error.
* If there is no callback function, stop the message loop.
Through this mechanism, React can perform tasks when the main thread is idle and give up control when needed, ensuring the responsiveness of the page.
### 5.2 Task execution
The actual execution of the task is done in the `workLoop` function. This function is the core of the React scheduler and is responsible for cyclically executing tasks in the queue.
**File: packages/scheduler/src/Scheduler.js**
```javascript
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime
//Update tasks in the timer queue
advanceTimers(currentTime)
// Get the first task in the task queue
currentTask = peek(taskQueue)
while (currentTask !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
// Check whether the task has expired or whether it needs to give up execution rights
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
// The current task has not expired, but it has reached the time limit and execution needs to be interrupted.
break
}
const callback = currentTask.callback
if (typeof callback === 'function') {
//Reset the callback of the task to prevent repeated execution
currentTask.callback = null
//Set the current priority
currentPriorityLevel = currentTask.priorityLevel
// Check if the task has timed out
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime
//Execute task callback
const continuationCallback = callback(didUserCallbackTimeout)
//Update current time
currentTime = getCurrentTime()
if (typeof continuationCallback === 'function') {
// If the callback returns a function, it means that the task needs to continue execution
currentTask.callback = continuationCallback
} else {
// When the task is completed, remove it from the queue
if (currentTask === peek(taskQueue)) {
pop(taskQueue)
}
}
//Update the timer queue again
advanceTimers(currentTime)
} else {
// If the callback is not a function, directly remove the task from the queue
pop(taskQueue)
}
// Get the next task
currentTask = peek(taskQueue)
}
// Check if there is more work to do
if (currentTask !== null) {
return true
} else {
//If the task queue is empty, check the timer queue
const firstTimer = peek(timerQueue)
if (firstTimer !== null) {
// If there is a delayed task, schedule a new timeout callback
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime)
}
return false
}
}
```
The workflow of this function is as follows:
1. First update the timer queue and move the expired timer tasks to the main task queue.
2. Then start a loop to continuously take out tasks from the task queue for execution:
* Check whether the task has expired or needs to give up execution rights.
* If the task has not expired but the time limit has been reached, execution will be interrupted.
* Callback function for executing tasks.
* If the callback returns a new function, indicating that the task needs to continue execution, set this new function as the callback of the task.
* If the task is completed, remove it from the queue.
3. Every time a task is executed, the timer queue will be updated again to ensure that expired timer tasks are processed in a timely manner.
4. After the loop ends, if there are still tasks in the task queue, it returns true, indicating that there is still work to be done.
5. If the task queue is empty but there are tasks in the timer queue, arrange a new timeout callback to continue execution when the timer expires.
### 5.3 Task interruption and recovery
A core feature of React Fiber is interruptible rendering, which allows React to pause in the middle of a long-running task and return control to the browser, thus keeping the UI responsive. This mechanism is mainly implemented through task scheduling and time slicing, in which the `shouldYieldToHost` function plays a key role.
#### 5.3.1 shouldYieldToHost function
The `shouldYieldToHost` function determines whether the current task should be interrupted and control returned to the browser:
**File: packages/scheduler/src/Scheduler.js**
```javascript
function shouldYieldToHost() {
// Calculate the time elapsed from the start of the task to now
var timeElapsed = getCurrentTime() - startTime
// If the elapsed time is less than one frame, continue executing the task
if (timeElapsed < frameInterval) {
//The main thread is blocked for a short time, less than one frame.
// There is no need to give up control at this time and continue executing the current task.
return false
}
// Check if isInputPending API is enabled
if (enableIsInputPending) {
// Check if there are any pending drawing tasks
if (needsPaint) {
// There are pending drawing tasks, give up control immediately
return true
}
// Check if there is a pending discrete input event (such as a click)
if (timeElapsed < continuousInputInterval) {
// If the blocking time is not long, only give up control when there is a pending discrete input
return isInputPending()
}
// Check if there are any pending input events of any type
if (timeElapsed < maxInterval) {
// Has been blocked for a while, but still within acceptable limits
// Only yield control if there is pending input (discrete or continuous)
return isInputPending(continuousOptions)
}
// Has been blocked for a long time, give up control unconditionally
return true
} else {
//Always yield control if isInputPending API is not available
return true
}
}
```
This function has several factors to decide whether to interrupt the task:
1. Task execution time
2. Whether there are any pending drawing tasks
3. Whether there is pending user input (distinguish between discrete input and continuous input)
4. Does the browser support the `isInputPending` API?
#### 5.3.2 Implementation of task interruption and recovery
The interruption and recovery of tasks are mainly implemented in React’s work loop:
**File: packages/react-reconciler/src/ReactFiberWorkLoop.js**
```javascript
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
```
In this concurrent mode work loop, React will continuously check the `shouldYield()` function (internally `shouldYieldToHost()` will be called). If control needs to be ceded, React will interrupt the current rendering process.
Task recovery is handled by React’s scheduler:
```javascript
function performConcurrentWorkOnRoot(root, didTimeout) {
// ...
do {
try {
if (didTimeout) {
renderRootSync(root, lanes)
} else {
renderRootConcurrent(root, lanes)
}
break
} catch (thrownValue) {
handleError(root, thrownValue)
}
} while (true)
// ...
if (root.callbackNode === originalCallbackNode) {
// If the currently executing task node is the same as the scheduled node,
//Return a callback function to continue execution
return performConcurrentWorkOnRoot.bind(null, root)
}
return null
}
```
If the task is interrupted, the `performConcurrentWorkOnRoot` function returns a continue callback, allowing the scheduler to resume the task later.
#### 5.3.3 Priority management and task switching
React also uses priority management to determine the execution order of tasks:
```javascript
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// There is no change in priority and existing tasks can be reused.
return;
}
if (existingCallbackNode != null) {
// Cancel the existing callback and prepare to schedule a new callback
cancelCallback(existingCallbackNode);
}
// Schedule new callback
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Synchronization priority uses special internal queue
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
```
This function ensures that the root node is scheduled correctly. It will decide whether to interrupt the current task and schedule a new high-priority task based on the priority of the task.
Through this complex task interruption, recovery and priority management mechanism, React can remain responsive to user input and provide a smooth user experience when executing long-term tasks.
In practical applications, we can use this mechanism to optimize performance, such as splitting large computing tasks into small pieces, using `React.useCallback` and `React.useMemo` to cache calculation results, or using `React.lazy` and `Suspense` to delay loading of components that are not urgently needed.
1. React.useCallback and React.useMemo
These two hooks are used for performance optimization. They cache calculation results to avoid unnecessary recalculation, thereby reducing rendering time. Their essence is to give more time to high-priority tasks.
**File: packages/react-reconciler/src/ReactFiberHooks.js**
```javascript
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
```
This is an implementation of `useMemo`. It will check whether the dependency has changed, and if there is no change, it will directly return the cached value to avoid recalculation. This reduces unnecessary calculations and leaves more time for other high-priority tasks.
2. React.lazy and Suspense
These two APIs allow us to lazy load components, reduce initial load time, and display fallback content during the loading process.
**File: packages/react/src/ReactLazy.js**
```javascript
function lazy<T>(
ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {
_status: Uninitialized,
_result: ctor,
};
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,
};
return lazyType;
}
```
This is an implementation of `React.lazy`. It creates a special component type that triggers asynchronous loading when rendered.
**File: packages/react-reconciler/src/ReactFiberThrow.js**
```javascript
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
) {
// ...
if (value !== null && typeof value === 'object' && typeof value.then === 'function') {
// This is a wakeable.
const wakeable: Wakeable = (value: any);
// ...
// Schedule the nearest Suspense to re-render the timed out view.
const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
if (suspenseBoundary !== null) {
suspenseBoundary.flags &= ~ForceClientRender;
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
returnFiber,
sourceFiber,
root,
rootRenderLanes,
);
// We only attach ping listeners in concurrent mode.
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
return;
}
// ...
}
// ...
}
```
When a Promise is encountered, React looks for the nearest Suspense boundary and lets it re-render. This allows React to display fallback content while asynchronous content is loading without blocking the rendering of the entire application.
That is to say
1. `useCallback` and `useMemo` leave more time for other high-priority tasks that may be interrupted by reducing unnecessary calculations and rendering.
2. `React.lazy` and `Suspense` allow React to pause rendering when encountering a component that has not yet been loaded, display fallback content, and then resume rendering after the component is loaded. This entire process is interruptible, allowing React to handle higher priority tasks when necessary.
For example, consider the following scenario:
```jsx
const HeavyComponent = React.lazy(() => import('./HeavyComponent'))
function App() {
const [showHeavy, setShowHeavy] = useState(false)
const heavyCalculation = useMemo(() => computeExpensiveValue(a, b), [a, b])
return (
<div>
<button onClick={() => setShowHeavy(true)}>Load Heavy Component</button>
<p>{heavyCalculation}</p>
<Suspense fallback={<div>Loading...</div>}>{showHeavy && <HeavyComponent />}</Suspense>
</div>
)
}
```
In this example:
1. `useMemo` ensures that `heavyCalculation` is only recalculated when `a` or `b` changes, avoiding expensive calculations every time it is rendered.
2. `React.lazy` and `Suspense` allow `HeavyComponent` to be loaded lazily. When the user clicks the button, React will start loading the component, but instead of blocking the UI, it will display the fallback content.
3. During the entire process, if there are higher-priority tasks (such as user input), React can interrupt the current task, process the high-priority task, and then resume the previous rendering.
This is actually how these APIs work with React's task interruption, recovery, and priority management mechanisms, and it is also the scenario we will encounter during actual development.
### 5.4 Priority management and solution to the hunger problem
React's scheduling system not only needs to handle the execution of tasks, but also needs to solve the problem of low-priority task starvation (never executed) caused by continued high-priority tasks.
1. **Mark Hungry Lane**
On every scheduled update, React calls the `ensureRootIsScheduled` function, which calls `markStarvedLanesAsExpired` to check if there are any lanes that are "starved" by other work.
**File: packages/react-reconciler/src/ReactFiberWorkLoop.js**
```javascript
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are starved by other work. If there are, mark them as expired.
markStarvedLanesAsExpired(root, currentTime);
// ... [rest of the function]
}
```
2. **Calculate expiration time**
For each pending lane, React calculates an expiration time. This expiration time varies based on the priority of the lane.
**File: packages/react-reconciler/src/ReactFiberLane.js**
```javascript
function computeExpirationTime(lane: Lane, currentTime: number) {
switch (lane) {
case SyncLane:
case InputContinuousLane:
return currentTime + 250;
case DefaultLane:
case TransitionLane1:
case TransitionLane16:
return currentTime + 5000;
// ... [other cases]
}
}
```
3. **Mark expired lanes**
On subsequent schedule updates, React checks each lane to see if it has expired. If expired, the lane will be marked on `root.expiredLanes`.
**File: packages/react-reconciler/src/ReactFiberLane.js**
```javascript
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number
): void {
const pendingLanes = root.pendingLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}
```
4. **Handle expired lanes**
When performing an update, React checks if there are any expired lanes. If there are, these tasks will be promoted to synchronization priority for processing.
**File: packages/react-reconciler/src/ReactFiberWorkLoop.js**
```javascript
function performConcurrentWorkOnRoot(root) {
// ...
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout)
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes)
// ...
}
```
Through this mechanism, React ensures that even when there are a large number of high-priority tasks, low-priority tasks are eventually executed, thus solving the starvation problem.
## 6\. Coordination process between task queue and React
The task queue is not just an independent scheduling system, it is closely connected with React's coordination process. In this process, React's Lane Priority and Scheduler Priority play an important role. These two priority systems have different functions and performances in practical applications.
### 6.1 Practical application of Lane Priority
Lane priority is mainly used for update scheduling inside React. It allows React to have more fine-grained control over the priority of different types of updates. For example:
1. **User interaction**: such as click events, are usually given high priority lanes.
```jsx
function Button() {
const [count, setCount] = useState(0)
const handleClick = () => {
// This update will be assigned a high priority lane
setCount(count + 1)
}
return <button onClick={handleClick}>Clicked {count} times</button>
}
```
2. **Data Acquisition**: Usually assigned the default priority lane.
```jsx
function DataFetcher() {
const [data, setData] = useState(null)
useEffect(() => {
// This update will be assigned the default priority lane
fetchData().then(setData)
}, [])
return data ? <div>{data}</div> : <div>Loading...</div>
}
```
3. **Background calculation**: Low priority lanes can be used.
```jsx
function ExpensiveComponent() {
const [result, setResult] = useState(null)
useEffect(() => {
// Use startTransition to mark updates as low priority
startTransition(() => {
const computedResult = expensiveComputation()
setResult(computedResult)
})
}, [])
return result ? <div>{result}</div> : <div>Computing...</div>
}
```
### 6.2 Practical application of Scheduler Priority
Scheduler priority is mainly used to determine the execution order of tasks in the JavaScript runtime. It directly affects when the task is executed. Let me write pseudo code.
For example:
1. **Immediate priority**: used for tasks that need to be performed immediately, such as animation or dragging.
```jsx
function AnimatedComponent() {
const [position, setPosition] = useState(0)
useEffect(() => {
const animate = () => {
Scheduler.unstable_runWithPriority(Scheduler.unstable_ImmediatePriority, () => {
setPosition((prev) => prev + 1)
})
requestAnimationFrame(animate)
}
animate()
}, [])
return <div style={{ transform: `translateX(${position}px)` }} />
}
```
2. **User blocking priority**: used for tasks that require quick response but do not require immediate execution, such as text input.
```jsx
function SearchInput() {
const [query, setQuery] = useState('')
const handleChange = (e) => {
Scheduler.unstable_runWithPriority(Scheduler.unstable_UserBlockingPriority, () => {
setQuery(e.target.value)
})
}
return <input value={query} onChange={handleChange} />
}
```
3. **Normal priority**: used for updates that do not require immediate response, such as network requests.
```jsx
function DataFetcher() {
const [data, setData] = useState(null)
useEffect(() => {
Scheduler.unstable_runWithPriority(Scheduler.unstable_NormalPriority, () => {
fetchData().then(setData)
})
}, [])
return data ? <div>{data}</div> : <div>Loading...</div>
}
```
Priority plays a key role in the task scheduling process. High-priority tasks (such as user input) interrupt low-priority tasks (such as data retrieval). This is achieved by comparing the priority of the currently executing task with the priority of the incoming task.
**File: packages/scheduler/src/Scheduler.js**
```javascript
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break
default:
priorityLevel = NormalPriority
}
var previousPriorityLevel = currentPriorityLevel
currentPriorityLevel = priorityLevel
try {
return eventHandler()
} finally {
currentPriorityLevel = previousPriorityLevel
}
}
```
This function allows running an event handler at a specific priority level. It temporarily changes the current priority level, executes the processor, and then restores the original priority level. This ensures that high-priority operations respond promptly without being blocked by low-priority tasks.
### 6.3 Mixed priorities
Example:
```jsx
function ComplexComponent() {
const [text, setText] = useState('')
const [list, setList] = useState([])
const [isPending, startTransition] = useTransition()
const handleInputChange = (e) => {
const newText = e.target.value
// High priority update: update the input box immediately
setText(newText)
// Low priority update: update the list in the transition
startTransition(() => {
const newList = generateLargeList(newText)
setList(newList)
})
}
return (
<div>
<input value={text} onChange={handleInputChange} />
{isPending ? <p>Updating list...</p> : null}
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
```
In this example:
1. **Lane Priority**:
* The update of the input box (`setText`) is given a high priority lane because it responds directly to user input.
* Updates to the list (`setList`) are wrapped in `startTransition` and given a low priority lane as it is a potentially time consuming operation.
2. **Scheduler Priority**:
* React internally converts updates from high priority lanes (input boxes) to the scheduler's high priority (e.g. `UserBlockingPriority`).
* Updates (lists) of low priority lanes will be converted to lower scheduler priorities (such as `NormalPriority`).
3. **Priority collaborative work**:
* As the user types, the input box updates immediately, providing instant feedback.
* At the same time, list updates are marked as low priority, allowing React to process them when the main thread is idle.
* If the user continues typing, new high-priority updates (input boxes) interrupt ongoing low-priority updates (lists), ensuring the interface is always responsive.
* When input stops, React will complete the interrupted list update.
4. **Status Management**:
* The `isPending` state allows us to display a loading indicator when low-priority updates are in progress, improving user experience.
### 6.4 In source code implementation
1. **Lane Priority**:
* Used during React's coordination process.
* Used to mark the urgency and importance of updates.
* Determines the order in which updates are processed internally by React.
2. **Scheduler Priority**:
* Used in React's scheduler.
* Determine the order in which tasks are executed in the JavaScript runtime.
* Control when tasks are executed.
3. **Task Management**:
* Involves lane priority and scheduler priority.
* Use lane priority to mark task importance.
* Convert lane priorities to scheduler priorities to determine the execution order of tasks.
4. **Coordination Process**:
* Mainly uses lane priority.
* Decide which updates should be prioritized.
* Consider lane priority when building and comparing Fiber trees.
5. **Priority conversion**:
* Occurs when a task is scheduled.
* React internally converts lane priorities into scheduler priorities.
* This transformation ensures that React's internal priority system works together with the scheduler's priority system.
```javascript
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are starved by other work. If there are, mark them as expired so we know what to do with them next.
markStarvedLanesAsExpired(root, currentTime);
// Determine which lanes to process next, and their priorities.
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// If there are no lanes to process, cancel the existing callback and return
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// Use the highest priority lane to indicate callback priority
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there is an existing task. We can probably reuse it.
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
//Priority has not changed. We can reuse existing tasks.
return;
}
// If there is an existing callback node, cancel it
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
// Schedule a new callback
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: synchronous React callbacks are scheduled in a special internal queue
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else {
//Convert lane priority to scheduler priority
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
// Schedule new callbacks using scheduler priority
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
//Update the callback priority and callback point of the root node
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
```
This function is responsible for ensuring that the root Fiber node is scheduled correctly. It checks if there is new work to be performed and creates or reuses tasks based on priority.
### 6.5 Lane and priority switching
Before understanding the source code, we need to deepen the meaning of these two priorities again. "Boring" understanding of the source code is "meaningless".
1. Scheduler Priority:
* This is the key factor that ultimately determines the order of task execution.
* 它直接与 JavaScript 运行时交互,决定哪些任务应该先执行,哪些可以稍后执行。
* `调度器优先级是 React 与底层 JavaScript 引擎通信的"语言"。 `
2. 车道优先级(Lane Priority):
* 这是 React 内部使用的一个更细粒度的优先级系统。
* 它允许 React 更精确地表达不同类型更新的重要性和紧急程度。
* `车道优先级可以看作是 React 的内部"思考过程",用于决定不同更新的相对重要性。 `
3. 优先级转换:
* React 需要将其内部的车道优先级转换为调度器可以理解的优先级。
* `这个转换过程可以看作是 React 的"决策过程",将其内部的复杂优先级系统映射到调度器的相对简单的优先级系统上。 `
```javascript
export function lanePriorityToSchedulerPriority(lanePriority: LanePriority): ReactPriorityLevel {
switch (lanePriority) {
case SyncLanePriority:
case SyncBatchedLanePriority:
return ImmediatePriority;
case InputDiscreteHydrationLanePriority:
case InputDiscreteLanePriority:
case InputContinuousHydrationLanePriority:
case InputContinuousLanePriority:
return UserBlockingPriority;
case DefaultHydrationLanePriority:
case DefaultLanePriority:
case TransitionHydrationPriority:
case TransitionPriority:
case SelectiveHydrationLanePriority:
case RetryLanePriority:
return NormalPriority;
case IdleHydrationLanePriority:
case IdleLanePriority:
case OffscreenLanePriority:
return IdlePriority;
case NoLanePriority:
return NoSchedulerPriority;
default:
invariant(
false,
'Invalid update priority: %s. This is a bug in React.',
lanePriority,
);
}
}
```
这个函数负责将React的车道优先级转换为调度器的优先级。这种转换关系如下:
1. 同步优先级(Sync)对应调度器的立即优先级(Immediate)
2. 输入优先级(Input)对应用户阻塞优先级(UserBlocking)
3. 默认优先级(Default)和过渡优先级(Transition)对应普通优先级(Normal)
4. 空闲优先级(Idle)对应调度器的空闲优先级(Idle)
这种转换确保了React内部的优先级系统能够与调度器的优先级系统协同工作,从而实现更精细的任务调度。
## 7\. 任务队列与React并发模式
在我们上述讲的所有内容都是达成 `并发模式` 的拼图,首先我们需要抓住关键词 `并发模式`,而不是实现 `并发`,`JavaScript` 是单线程的,传统意义上的并发(如多线程)是同时处理多个任务。
### 7.1 并发本质
它的本质是:
1. **交错执行**:在单一线程上交错执行多个任务。
2. **优先级调度**:根据任务的优先级决定执行顺序。
3. **可中断渲染**:允许高优先级任务打断低优先级任务。
想象你正在做一个大型拼图(低优先级任务,如渲染一个复杂列表),突然电话响了(高优先级任务,如用户输入)。在 React 的并发模式下:
1. 你可以暂停拼图(中断渲染)。
2. 接听电话(处理高优先级任务)。
3. 通话结束后,继续拼图(恢复渲染)。
代码层面的实现:
```javascript
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
function shouldYield() {
return (
getCurrentTime() >= deadline || // 时间片用完
hasHigherPriorityWork() // 有更高优先级的工作
)
}
```
这里的 `shouldYield()` 函数检查是否应该让出控制权。如果时间片用完或有更高优先级的工作,React 就会暂停当前任务。
### 7.2 为什么这也算是"并发"?
React的任务管理机制、任务中断恢复和车道协调共同构成了实现伪并发的核心框架。
1. **多任务交错执行**:尽管在任何给定时刻只有一个任务在执行,但从宏观角度看,多个任务是交错进行的。这种交错执行创造了任务并行的错觉,类似于操作系统的时间片轮转调度。
2. **非阻塞**:低优先级任务不会阻塞高优先级任务的执行。这确保了重要的用户交互和更新能够及时响应,提高了应用的整体响应性。
3. **响应式**:系统能够快速响应用户交互,即使在处理大量后台工作时也是如此。这种能力模拟了真正并发系统的行为,多个处理单元同时处理不同的任务。
4. **公平调度**:所有任务最终都会得到处理,只是执行顺序根据优先级动态调整。这种调度策略确保了资源的公平分配,避免了任务饥饿问题。
5. **细粒度控制**:通过车道优先级系统,React能够对任务进行更细粒度的控制,实现了类似于抢占式多任务处理的效果。
6. **异步渲染**:React的并发模式支持异步渲染,允许组件"暂停"渲染等待数据,这种能力进一步增强了并发的特性。
总的来说,虽然React的 `并发模式` 在技术实现上与传统的多线程并发有所不同,但它在功能和效果上实现了类似的目标:提高系统的响应性、优化资源利用、平衡多个任务的执行。
这就是 `react` 的核心。
## 8\. 总结
1. **灵活的优先级系统**:React使用多级优先级系统,能够精确控制不同类型任务的执行顺序,确保重要的更新能够及时响应。
2. **高效的数据结构**:使用小顶堆实现的优先级队列,保证了任务的快速插入和获取,时间复杂度为O(log n)。
3. **可中断的执行模型**:通过时间切片和yield机制,React能够在长任务执行过程中适时让出主线程,保证页面的响应性。
4. **与React核心的紧密集成**:任务队列与React的协调过程、Lanes系统等核心概念紧密结合,支持了并发模式等高级特性。
5. **性能优化**:通过批量更新、延迟任务处理等机制,任务队列在保证功能的同时也兼顾了性能。
其实看这些源码,都是为了写出更高效、更流畅的React应用。
平时大家是靠感觉来使用这些优化手段,那么在理解这个任务队列和调度之后,会让我们心里更有底,也是知其所也然了。