It’s a New Year and React has been quietly implemented in Suspense for the past year, bringing in React Hooks and major changes to the source code. So it’s important to re-read how it implements these features and what concurrent rendering is ready to open up in the future.

In the early stages of React16, the original DIff process was broken up into two phases. The first phase is called Reconcile, which is the original diff virtual DOM process. There are a number of methods involved in this period called reconcileXXX, For reconcileSingleElement, reconcileSinglePortal, reconcileChildFibers, reconcileSingleTextNode, ReconcileChildrenArray, reconcileChildrenIterator, reconcileChildFibers… Component instances and real DOM nodes are created and some lightweight hooks are performed. The second phase is called commit. In concurrent reconciliations, there may be only one COMMIT. Commit is to surface the effects below the surface, such as inserting the node into the DOM tree, fixing the attribute style text content of the node, performing weight hooks such as componentDidXXX, and performing ref operations (which may involve DOM operations).

Schedule source code analysis

React now adds a new phase, Schedule, to precede these two phases. Because a page can have multiple reactdom.render, there are often multiple components in the virtual DOM tree that have self-updating capabilities. (React Hooks make stateless components self-updating as well.) In concurrent mode, the component setState does not update the view immediately, so there are multiple components to update in a period of time called root (the starting point for rendering), but who is the real nextRoot? You need a scheduling algorithm to make that decision. React assigns each component an expiration time (priority) based on the current time. The higher the number, the higher the priority.

The starting point method for schedule is scheduleWork. ReactDOM. Render, setState, forceUpdate, React Hooks dispatchActions go through scheduleWork.

ScheduleWork has a scheduleWorkToRoot method that delays (or increases) the current Fiber and alternate expiration time. Because in ReactFiber, expiration time is equivalent to priority, in other words, the more frequently a component sets state during a certain period of time, the more priority it updates.

In concurrent mode, setState is executed after 33ms (if in animation, increase to 100ms interval for smoothness). If the updated node is a controlled component (input), then it goes directly to the ActiveUpdates method, without going through scheduleWork, and is updated immediately! React also has a batchedUpdates method that is not registered in the document. This method can cause a large number of nodes to update immediately, and ignores shouldComponentUpdate return false!! Privileged methods like batchedUpdates already exist in React16!


Above is the general flow of ReactFiber. The green methods at the bottom are aristocratic methods, with high priority.

// function scheduleWork(fiber, expirationTime) {const root = scheduleWorkToRoot(fiber, expirationTime) { expirationTime); if (root === null) { return; } if ( ! isWorking && nextRenderExpirationTime ! == NoWork && expirationTime > nextRenderExpirationTime ) { resetStack(); } markPendingPriorityLevel(root, expirationTime); If (// Render another node if in the prepare or commitRoot phase! isWorking || isCommitting || // ... unless this is a different root than the one we're rendering. nextRoot ! == root ) { const rootExpirationTime = root.expirationTime; requestWork(root, rootExpirationTime); }}Copy the code

RequestWork decides how to enter performWorkOnRoot. If you’ve already started rendering, you can go back to performWorkOnRoot, performSyncWork, performSync to performWork to performWorkOnRoot, Four is an asynchronous method, enter from scheduleCallbackWithExpirationTime performAsyncWork to performWork to performWorkOnRoot.

PerformWork is used to decide whether to render synchronously or asynchronously. The first line of performWork is the findHighestPriorityRoot method, which picks out the root with the highest priority and throws it to performWorkOnRoot.

PerformWorkOnRoot Determines whether to execute commitRoot directly or renderRoot first and then commitRoot.

The completeRoot is just a simple wrapper around a renderRoot that does something that at least we don’t use ReactBatch.

The whole process is as follows:

scheduleWork --> requestWork --> performWork --> findHighestPriorityRoot -->
performWorkOnRoot --> completeRoot --> renderRoot --> commitRoot
Copy the code

The schedule phase from scheduleWork to completeRoot determines which subtree takes precedence and when.

RenderRoot is the Reconcile phase, in which components generate child virtual DOM, component instance and real DOM, and various effectTags.

CommitRoot, the commit phase, updates the view and executes heavy hooks and refs.

Commit class source code analysis

React updates during the COMMIT phase. As described above, fiber is tagged in the Reconcile phase. Fiber objects are the virtual DOM of the new age, hosting important data such as component instances and the real DOM. These important data do not need to be regenerated during the update process. React, however, wants to branch like Git and roll back in case of an error. So Fiber has an alternate attribute, you can call it the alternate, you can call it the Ghost, you can call it the Thunder. So let’s just open up the formUnit of work method

function performUnitOfWork(workInProgress) { var current = workInProgress.alternate; next = beginWork(current, workInProgress, nextRenderExpirationTime); / /... }Copy the code

WorkInProgress is a fiber. At the beginning, it has no alternate, beginWork is to perform mount or update operation according to whether it has alternate. And the great has alternate in the beginning, it is always updated, its

// Function createFiberRoot(containerInfo, isConcurrent, hydrate) { var uninitializedFiber = createHostRootFiber(isConcurrent); var root = { current: uninitializedFiber, containerInfo: containerInfo, pendingChildren: null, //... Slightly} uninitializedFiber. StateNode = root; return root; }Copy the code

Root from scheduleWork to completeRoot refers to HostRootFiber’s current property, which is itself a fiber.

There is something called finishedWork in performWorkOnRoot, which has been used in many methods since then. As you can see from debugging, before renderRoot, root had no finishedWork property, but after renderRoot, it had finishedWork property. See that this method was added to the onComplete method in renderRoot.

var rootWorkInProgress = root.current.alternate; 
// Ready to commit.
 onComplete(root, rootWorkInProgress, expirationTime);

function onComplete(root, finishedWork, expirationTime) {
  root.pendingCommitExpirationTime = expirationTime;
  root.finishedWork = finishedWork;
}
Copy the code

So where does root.current. Alternate come from? Only createWorkInProgress can create a copy of Fiber. The first half of renderRoot has these lines

if (expirationTime ! == nextRenderExpirationTime || root ! == nextRoot || nextUnitOfWork === null) { // Reset the stack and start working from the root. resetStack(); nextRoot = root; nextRenderExpirationTime = expirationTime; nextUnitOfWork = createWorkInProgress(nextRoot.current, null, nextRenderExpirationTime); root.pendingCommitExpirationTime = NoWork; }Copy the code

With finishedWork, the all-important nextEffect is available on commitRoot.

 firstEffect = finishedWork.firstEffect;
 nextEffect = firstEffect
Copy the code

NextEffect is a global variable. Reactfiberscheduler.js is a 2500 line file that defines a large number of global variables and hundreds of lines of giant functions.

The commit phase is four commitXXX method, commitBeforeMutationLifecycles, commitAllHostEffects, nextEffect commitAllLifeCycles will be used. CommitPassiveEffects is more humane and directly bind firstEffect. CommitPassiveEffects is the latest hook to execute in React Hooks.

At this time, we need to study how finishedWork. FirstEffect comes from. FinishedWork is created by createWorkInProgress on HostRootFiber’s current object. In completeUnitOfWork there is a line like this:

var effectTag = workInProgress.effectTag; // Skip both NoWork and PerformedWork tags when creating the effect list. // PerformedWork effect is read by React DevTools but shouldn't be committed. if (effectTag > PerformedWork) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; }Copy the code

Each parent fiber stores its updated children as lastEffect or lasteffect.nexteffect. Its children could have been array structures or arrays containing arrays, but now all are linked lists.

There is also a completeWork method inside completeUnitOfWork, which immediately adds the newly generated child to the parent element.

var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress); appendAllChildren(instance, workInProgress, false, false); AppendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. var node = workInProgress.child; while (node ! == null) { if (node.tag === HostComponent || node.tag === HostText) { appendInitialChild(parent, node.stateNode); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child ! == null) { node.child.return = node; node = node.child; continue; } if (node === workInProgress) { return; } while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; }}; function appendInitialChild(parentInstance, child) { parentInstance.appendChild(child); }Copy the code

So in the COMMIT phase, it only has to move, delete, modify styles and text for element nodes (side effects)

The other commitXXX are simple.

CommitBeforeMutationLifecycles getSnapshotBeforeUpdate is execution

CommitAllHostEffects performs DOM node-related operations

CommitAllLifeCycles performs operations related to component instances

Suspense and lazy loading implementation

Suspense is a virtual component, so if it isn’t a LazyComponent (generated via react. lazy) directly below it, it doesn’t render itself, like fragments, profilers, and StrictMode.

// Use dynamic import statements. Function (){return new Promise(function(resolve){function(resolve){function(resolve){ SetTimeout (function(){resolve()}, 1500) }).then(function(){//then method must return an object with default property return {default: Function (){return <div> </div>}}})}); function App (){ return <div> <React.Suspense fallback={<div>Loading... </div>}> <LazyComponent /> </React.Suspense> </div> } ReactDOM.render(<App />, container)Copy the code

Let’s see how Fiber handles it, from renderRoot to workLoop to FormUnitofWork. Formunitofwork will process all fibers one by one. Form unitof work is divided into beginWork and completeWork stages. When beginWork encounters a SuspenseComponent, it drops the updateSuspenseComponent method, If (workInProgress.effectTag & DidCapture) === NoEffect) {} nextDidTimeout is false and the child LazyComponent is resolved directly.

 child = next = mountChildFibers(workInProgress, null,
 nextPrimaryChildren, renderExpirationTime);
Copy the code

When LazyComponent is encountered in mountChildFibers, it calls mountLazyComponent, which in turn calls readLazyComponentType. ReadLazyComponentType reads the component’s _status property to determine whether to return the Result component or throw an error. Pretty much everything was wrong except in Resolved cases.

function readLazyComponentType(lazyComponent) { var status = lazyComponent._status; var result = lazyComponent._result; switch (status) { case Resolved: var Component = result; return Component; case Rejected: var error = result; throw error; case Pending: var thenable = result; throw thenable; default: lazyComponent._status = Pending; var ctor = lazyComponent._ctor; var _thenable = ctor(); _thenable.then(function (moduleObject) { if (lazyComponent._status === Pending) { var defaultExport = moduleObject.default; { if (defaultExport === undefined) { warning$1(false, 'lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\n\nYour code should look like: \n ' + "const MyComponent = lazy(() => import('./MyComponent'))", moduleObject); } } lazyComponent._status = Resolved; lazyComponent._result = defaultExport; } }, function (error) { if (lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; }}); lazyComponent._result = _thenable; throw _thenable; }}Copy the code

What if I throw it wrong? Don’t worry, workLoop is wrapped around a try catch.

do { try { workLoop(isYieldy); } catch (thrownValue) { if (nextUnitOfWork === null) { // This is a fatal error. didFatal = true; onUncaughtError(thrownValue); } else { var sourceFiber = nextUnitOfWork; var returnFiber = sourceFiber.return; if (returnFiber === null) { didFatal = true; onUncaughtError(thrownValue); } else {throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime); // Continue processing child or sibling nextUnitOfWork = completeUnitOfWork(sourceFiber); //nextUnitOfWork is the parent of lazyComponent, SuspenseComponent continue; } } } break; } while (true);Copy the code

The ‘continue’ statement indicates that nextUnitOfWork is the parent of the lazyComponent, so the SuspenseComponent goes to the workLoop. Then perform group of work, then beginWork, then update pen secomponent. NextDidTimeout is true, and the SuspenseComponent fallback function is retrieved, and the contents of the SuspenseComponent are resolved. !

Such a statement in throwException

/ / by SiTuZhengMei https://rubylouvre.github.io/nanachi/ if (_workInProgress. Tag = = = SuspenseComponent && shouldCaptureSuspense(_workInProgress.alternate, _workInProgress)) { // Found the nearest boundary. // If the boundary is not in concurrent mode, we should not suspend, and // likewise, when the promise resolves, we should ping synchronously. var pingTime = (_workInProgress.mode & ConcurrentMode) === NoEffect ? Sync : renderExpirationTime; // Attach a listener to the promise to "ping" the root and retry. var onResolveOrReject = retrySuspendedRoot.bind(null, root, _workInProgress, sourceFiber, pingTime); if (enableSchedulerTracing) { onResolveOrReject = unstable_wrap(onResolveOrReject); } thenable.then(onResolveOrReject, onResolveOrReject); }Copy the code

RetrySuspendedRoot resets scheduleWorkToRoot and starts a new rendering process. This time, the promised content will replace the nodes generated by fallback to achieve the lazyLoad effect!