This is the second article on the evolution of React architecture. The last one mainly introduced the update mechanism from synchronous to asynchronous, and this one focuses on the update process through loop iteration in Fiber architecture. The reason why loop iteration is used is that the recursive update process cannot be stopped once it starts, but can only continue downwards. Until the recursion ends or an exception occurs.
Implementation of recursive update
React 15’s recursive update logic is to place the components that need to be updated on a dirty component queue (as described in the React Architecture evolution from Synchronous to Asynchronous), then take the components out and recurse, constantly looking down at child nodes to see if they need to be updated.
The following code briefly describes this process:
updateComponent (prevElement, nextElement) {
if (
// If the component's type and key have not changed, update it
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
) {
// Text node updates
if (prevElement.type === 'text') {
if(prevElement.value ! == nextElement.value) {this.replaceText(nextElement.value)
}
}
// Update the DOM node
else {
// Update the DOM attributes first
this.updateProps(prevElement, nextElement)
// Update children
this.updateChildren(prevElement, nextElement)
}
}
// If the type and key of the component change, re-render the component directly
else {
// Triggers the unmount life cycle
ReactReconciler.unmountComponent(prevElement)
// Render the new component
this._instantiateReactComponent(nextElement)
}
},
updateChildren (prevElement, nextElement) {
var prevChildren = prevElement.children
var nextChildren = nextElement.children
// Omit the diff process for reordering by key
if (prevChildren === null) {}// Render a new child node
if (nextChildren === null) {}// Clear all child nodes
// Compare child nodes
prevChildren.forEach((prevChild, index) = > {
const nextChild = nextChildren[index]
// Recursive process
this.updateComponent(prevChild, nextChild)
})
}
Copy the code
To see this process more clearly, let’s write a simple Demo that constructs a 3 by 3 Table component.
// https://codesandbox.io/embed/react-sync-demo-nlijf
class Col extends React.Component {
render() {
// Pause 8ms before rendering to put a little pressure on render
const start = performance.now()
while (performance.now() - start < 8)
return <td>{this.props.children}</td>}}export default class Demo extends React.Component {
state = {
val: 0
}
render() {
const { val } = this.state
const array = Array(3).fill()
// create a 3 * 3 table
const rows = array.map(
(_, row) = > <tr key={row}>
{array.map(
(_, col) => <Col key={col}>{val}</Col>
)}
</tr>
)
return (
<table className="table">
<tbody>{rows}</tbody>
</table>)}}Copy the code
Then update the Table value every second, making val + 1 each time, from 0 to 9.
// https://codesandbox.io/embed/react-sync-demo-nlijf
export default class Demo extends React.Component {
tick = () = > {
setTimeout(() = > {
this.setState({ val: next < 10 ? next : 0 })
this.tick()
}, 1000)}componentDidMount() {
this.tick()
}
}
Copy the code
The complete code online address: codesandbox. IO/embed/react… . Each time the Demo component calls setState, React will determine whether the type of the component has changed. If it has, it will re-render the entire component. If it has not, it will update the state. The TR component then moves down to the TD component and finally finds that the text node under the TD component has been modified and updated through the DOM API.
This process can also be seen clearly in Performance’s function call stack. UpdateChildren after updateComponent will continue to call updateComponent of child components until all components are recursed, indicating that the update is complete.
The downside of recursion is obvious: you can’t pause updates. Once you start, you have to start from beginning to end. This doesn’t match React 16’s idea of splitting time slices to give the browser a break.
Recyclable Fiber
The linked list structure mentioned here is Fiber. The biggest advantage of the linked list structure is that it can be traversed in a circular way. As long as the current traversal position is remembered, it can be quickly restored and traversed again even after interruption.
Let’s first look at the data structure of a Fiber node:
function FiberNode (tag, key) {
// key is used to optimize list diff
this.key = key
// Node type; FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
this.tag = tag
/ / child nodes
this.child = null
/ / the parent node
this.return = null
// Sibling nodes
this.sibling = null
// Update the queue to hold the value of setState temporarily
this.updateQueue = null
// Node update expiration time, used for time sharding
// react 17 change to 'lanes' and' childLanes'
this.expirationTime = NoLanes
this.childExpirationTime = NoLanes
// Corresponds to the actual DOM node of the page
this.stateNode = null
// A copy of the Fiber node, which can be understood as a spare tire, is mainly used to improve the performance of updates
this.alternate = null
}
Copy the code
For example, here we have plain HTML text:
<table class="table">
<tr>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
</tr>
</table>
Copy the code
In previous React versions, JSX converted to the createElement method to create a virtual DOM with a tree structure.
const VDOMRoot = {
type: 'table'.props: { className: 'table' },
children: [{type: 'tr'.props: {},children: [{type: 'td'.props: {},children: [{type: 'text'.value: '1'}]}, {type: 'td'.props: {},children: [{type: 'text'.value: '1'}}]}, {type: 'tr'.props: {},children: [{type: 'td'.props: {},children: [{type: 'text'.value: '1'}]}]}Copy the code
In Fiber architecture, the structure is as follows:
// It's a bit simplified and doesn't match the real Fiber structure of React
const FiberRoot = {
type: 'table'.return: null.sibling: null.child: {
type: 'tr'.return: FiberNode, / / table FiberNode
sibling: {
type: 'tr'.return: FiberNode, / / table FiberNode
sibling: null.child: {
type: 'td'.return: FiberNode, / / tr FiberNode
sibling: {
type: 'td'.return: FiberNode, / / tr FiberNode
sibling: null.child: null.text: '1' // The child node has only text nodes
},
child: null.text: '1' // The child node has only text nodes}},child: {
type: 'td'.return: FiberNode, / / tr FiberNode
sibling: null.child: null.text: '1' // The child node has only text nodes}}}Copy the code
Implementation of cyclic updates
How does React perform Fiber traversal in setState?
let workInProgress = FiberRoot
// Traverse the Fiber node, stop traversing if the time slice runs out
function workLoopConcurrent() {
while( workInProgress ! = =null &&
!shouldYield() // Determines whether the current time slice expires
) {
performUnitOfWork(workInProgress)
}
}
function performUnitOfWork() {
const next = beginWork(workInProgress) // Return the child of the current Fiber
if (next) { / / the child
// Reset workInProgress to child
workInProgress = next
} else { // Child does not exist
// Back up the node
let completedWork = workInProgress
while(completedWork ! = =null) {
// Collect side effects, mainly used to indicate whether a node needs to manipulate the DOM
completeWork(completedWork)
/ / get Fiber. (
let siblingFiber = workInProgress.sibling
if (siblingFiber) {
// Sibling exists, then jump out of the complete process and continue beginWork
workInProgress = siblingFiber
return;
}
completedWork = completedWork.return
workInProgress = completedWork
}
}
}
function beginWork(workInProgress) {
// Call render method to create sub-fiber and diff
// Return the child of the current Fiber
return workInProgress.child
}
function completeWork(workInProgress) {
// Collect node side effects
}
Copy the code
Fiber traversal is essentially a loop, with a global workInProgress variable that stores the node currently diff by beginWork and then diff (render is called before diff). Recalculate state, prop) and return the first child of the current node (fiber.child) as the new working node until no child exists. Then, call the completedWork method on the current node to store the side effects generated in the beginWork process. If the current node has sibling nodes (fiber.sibling), change the working node to sibling nodes and re-enter the beginWork process. Until the completedWork returns to the root node, execute commitRoot to reflect any side effects into the real DOM.
In a traversal, each node goes through beginWork, completeWork, until it is returned to the root node, and all updates are committed via commitRoot. See React Technology Revealed.
The secret of time sharding
Said earlier, the structure of the Fiber traversal is to support the disruption recovery, in order to observe the process, we will before 3 * 3 Table component into Concurrent mode, online address: codesandbox. IO/embed/react… . Each TD part is paused once because each call to the Render part of the Col component takes 8ms, which is over one slice of time.
class Col extends React.Component {
render() {
// Pause 8ms before rendering to put a little pressure on render
const start = performance.now();
while (performance.now() - start < 8);
return <td>{this.props.children}</td>}}Copy the code
In this 3 * 3 component, there are 9 Col components, so there are 9 time-consuming tasks spread over 9 time slices, as can be seen from the Performance call stack:
In non-concurrent mode, Fiber node traversal is performed at once and does not split multiple time slices. The difference is that the workLoopSync method is called during the traversal and does not determine whether the time slice is used up.
// Traverses the Fiber node
function workLoopSync() {
while(workInProgress ! = =null) {
performUnitOfWork(workInProgress)
}
}
Copy the code
The shouldYield method determines whether the current time slice has run out, and this is the key to determining whether React is a synchronous or asynchronous render. ShouldYield’s method is simple, if not prioritized, to determine whether the current time has passed the preset deadline.
function getCurrentTime() {
return performance.now()
}
function shouldYield() {
// Get the current time
var currentTime = getCurrentTime()
return currentTime >= deadline
}
Copy the code
How did you get the deadline? Recalling the ChannelMessage mentioned in the last article (Evolution of the React Architecture — From Synchronous to asynchronous), updates are started via requestHostCallback (i.e. Port2. send) Sends asynchronous messages and receives the messages in performWorkUntilDeadline (port1. onMessage). PerformWorkUntilDeadline Each time a message is received, it indicates that it has entered the next task queue, at which point the deadline is updated.
var channel = new MessageChannel()
var port = channel.port2
channel.port1.onmessage = function performWorkUntilDeadline() {
if(scheduledHostCallback ! = =null) {
var currentTime = getCurrentTime()
// Reset the timeout period
deadline = currentTime + yieldInterval
var hasTimeRemaining = true
var hasMoreWork = scheduledHostCallback()
if(! hasMoreWork) {// There is no task left, change the status
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// There are tasks to be executed in the next task queue to give the browser a chance to breathe
port.postMessage (null); }}else {
isMessageLoopRunning = false;
}
}
requestHostCallback = function (callback) {
// Callback is mounted to scheduledHostCallback
scheduledHostCallback = callback
if(! isMessageLoopRunning) { isMessageLoopRunning =true
// Push a message, and the next queue queue calls callback
port.postMessage (null)}}Copy the code
The yieldInterval you are setting is a yieldInterval based on the current time, and the default yieldInterval is 5ms.
deadline = currentTime + yieldInterval
Copy the code
React also allows you to modify the yieldInterval by manually specifying the FPS to determine the specific time (ms) of a frame. The higher the FPS is, the shorter the time fragment is, and the higher the performance requirements on the device.
forceFrameRate = function (fps) {
if (fps < 0 || fps > 125) {
// The frame rate ranges from 0 to 125
return
}
if (fps > 0) {
// Generally 60 FPS devices
// A time sharding time is math.floor (1000/60) = 16
yieldInterval = Math.floor(1000 / fps)
} else {
// reset the framerate
yieldInterval = 5}}Copy the code
conclusion
Let’s concatenate asynchronous logic, circular updates, and time sharding. To review the sequence of setState calls in Concurrent mode:
Component.setState()
=> enqueueSetState()
=> scheduleUpdate()
=> scheduleCallback(performConcurrentWorkOnRoot)
=> requestHostCallback()
=> postMessage()
=> performWorkUntilDeadline()
Copy the code
ScheduleCallback methods will be introduced to the callback (performConcurrentWorkOnRoot) assembled into a task into taskQueue, then call requestHostCallback sends a message, enter the asynchronous task. PerformWorkUntilDeadline receives the asynchronous messaging, started from taskQueue task, the task here is before the incoming performConcurrentWorkOnRoot method, This method ends up calling workLoopConcurrent (workLoopConcurrent was covered earlier and will not be repeated). If workLoopConcurrent is interrupted due to timeout, hasMoreWork returns true and sends messages via postMessage, deferring the operation to the next task queue.
This is the end of the process, and I hope you found this article useful. The next article will cover the implementation of Hooks in The Fiber architecture.