preface

The React source reference version is 17.0.3. This is the React source series second, it is recommended that first look at the source of the students from the first to see, so more coherent, there are source series links below.

About the React @ 17

The React@17 version is actually a transitional version. As mentioned earlier, the fiber architecture was introduced after 16 to enable asynchronous interruptible updates, but the 17 version is still synchronized by default and cannot be interrupted. However, a Concurrent mode is added to the code to enable interrupt updates. Let us discuss the situation of the synchronous update legacyRenderSubtreeIntoContainer here, because of the asynchronous interruptible update more complex, we first groundwork, Concurrent mode put back again.

The preparatory work

For the sake of illustration, suppose we have the following code:

function App(){ const [count, setCount] = useState(0) useEffect(() => { setCount(1) }, []) const handleClick = () => setCount(count => count++) return (<div> <span style = "box-sizing: border-box! Important; word-wrap: break-word! Important; word-wrap: break-word! Important;" document.querySelector('#root'))Copy the code

In the React project, this JSX syntax is first compiled as:

React.createElement("App", null)
or
jsx("App", null)
Copy the code

The compilation method is not described here, interested can refer to:

Babel is compiled online

New JSX conversion

After the JSX syntax conversion, the React Element is converted to render as the first parameter of reactdom.render () via the creatElement or JSX API.

In the last article Fiber, we mentioned that a React project would have a fiberRoot and one or more Rootfibers. FiberRoot is the root node of a project. Current = rootFiber, where rootFiber is the root node of currentFiber tree.

if (! root) { // Initial mount root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate); fiberRoot = root._internalRoot; }Copy the code

After we create fiberRoot and rootFiber, we don’t know what to do next because they have nothing to do with our
function component. React creates the update and assigns the React element to reactdom.render () based on
as the first parameter.

var update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: null,
    callback: element,
    next: null
  };
Copy the code

Once you have this update, you also need to queue it for subsequent updates. It is important to explain the process of creating this queue, which is used many times in React.

var sharedQueue = updateQueue.shared; var pending = sharedQueue.pending; If (pending === null) {next = update; next = update; } else {// update; update. Next = pending. Next; update. pending.next = update; } // Pending points to the latest update, so that when we go through the list of updates, pending.next points to the first update that was inserted. sharedQueue.pending = update;Copy the code

The update queue is a circular list structure. Each time an update is added to the end of the list, the pointer points to that update and the update.next points to the first update:

As mentioned in the last article, React will have at most two fiber trees at the same time, a CurrentFiber tree and a workInProgressfiber tree. The root node of currentFiber tree has been created above, and the root node of workInProgressfiber tree will be created below by copying fiberRoot.current.

At this point, the preparation is done, and the next step is the main dish, where the loop is started, generating the Fiber tree and dom tree, and finally rendering them into the page.

Render phase

This stage is not to render the code onto the page, but to draw the corresponding Fiber tree and DOM tree based on our code.

workloopSync

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
Copy the code

In this loop, we will continue to find the corresponding child based on the workInProgress as the workInProgress of the next loop, until the leaf node is traversed, that is, the depth first traversal. In performUnitOfWork will perform the following beginWork.

beginWork

A brief description of beginWork is to generate a fiber tree.

fiber node

fiber node
fiber node
So loop to the bottom of the Niuniu text.

Note that in the above flow chart, updateFunctionComponent executes a function, renderWithHooks, that executes the App() function, which initializes all the hooks in the function. This is the useState() of the above example code.

When traversing to Niuniu text, there is no child under it, then beginWork’s work will come to a temporary end, why is it temporary, because in completeWork, if the fiber node traversed has siblings, it will go to beginWork again.

completeWork

When you iterate through the Niuniu text, you go to this completeWork.

Here, let’s briefly describe what completeWork does, which is to generate the DOM tree.

Generate the corresponding DOM node based on the fiber node and insert the previously generated DOM node into the currently created DOM node with this DOM node as the parent node. And will look up based on the incomplete workInProgressfiber tree generated in beginWork until fiberRoot. In this upward process, we will judge whether there is any sibling. If there is, we will beginWork again, and if there is no, we will continue to go upward. At the root node, a complete DOM tree is generated.

And by the way, there’s a code for that in completeWork

if (flags > PerformedWork) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = completedWork; } else { returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; }Copy the code

Flags > PerformedWork indicates that the current fiber node has side effects and needs to be added to the parent fiber’s effectList.

The commit phase

The main job at this stage is to deal with side effects. Side effects are uncertain operations, such as inserting, replacing, and deleting the DOM, as well as callbacks to useEffect() hooks.

commitWork

The preparatory work

Before commitWork, the workInProgressfiber tree generated in workloopSync is assigned to the finishedWork attribute of fiberRoot.

var finishedWork = root.current.alternate; // workInProgress fiber tree root. FinishedWork = finishedWork; FinishedLanes = Lanes; commitRoot(root);Copy the code

As mentioned above, if a fiber node has side effects it will be recorded to the parent fiber’s lastEffect nextEffect. In the code below, if the fiber tree has side effects, the rootFiber. FirstEffect node is treated as the first side effect firstEffect and the effectList is closed.

var firstEffect; If (finishedWork.flags > PerformedWork) {if (finishedWork.flags > PerformedWork) {if (finishedWork.flags > PerformedWork) (finishedWork.lastEffect ! == null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; }} else {// This rootFiber tree has no side effects firstEffect = finishedwork.firsteffect; }Copy the code

Before the mutation

A brief description of the work of the pre-mutation phase:

  • AutoFocus, blur logic after DOM node rendering/deletion
  • Call getSnapshotBeforeUpdate, fiberRoot and ClassComponent will go here;
  • Scheduling useEffect (asynchronous);

Before the mutation of the stage, traversing effectList list, execute commitBeforeMutationEffects method.

Before the do {/ / mutation invokeGuardedCallback (null, commitBeforeMutationEffects, null); } while (nextEffect ! == null);Copy the code

We entered the commitBeforeMutationEffects method, I will simplify code:

function commitBeforeMutationEffects() { while (nextEffect ! == null) { var current = nextEffect.alternate; // autoFocus/blur logic after DOM node rendering/deletion; if (! shouldFireAfterActiveInstanceBlur && focusedInstanceHandle ! == null){... } var flags = nextEffect.flags; // Call getSnapshotBeforeUpdate, fiberRoot and ClassComponent will go here if ((flags & Snapshot)! == NoFlags) {... } // set useEffect (asynchronous) if ((flags & Passive)! = = NoFlags) {/ / rootDoesHavePassiveEffects variable indicates whether the current has side effects if (! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; ScheduleCallback (NormalPriority$1, function () {flushPassiveEffects(); return null; }); NextEffect = nextEffect. NextEffect; }}Copy the code

In our sample code, we’ll focus on the third thing, scheduling useEffect (note that this is scheduling and does not execute immediately).

ScheduleCallback’s main job is to create a task:

Var newTask = {id: taskIdCounter++, callback: callback, priorityLevel: priorityLevel, startTime: startTime, expirationTime: expirationTime, sortIndex: -1 };Copy the code

It has a logic that says startTime and currentTime, if startTime > currentTime, it will add the task to the timerQueue, if startTime > currentTime, it will add the task to the taskQueue, Task.sortindex = expirationTime.

mutation

The mutation phase is simply described as being responsible for DOM rendering.

Differentiate between fibre. flags and perform different operations, such as resetting text, resetting ref, inserting, replacing, and deleting DOM nodes.

As in the previous mutation phase, the effectList is traversed and the commitMutationEffects method is executed.

Do {// mutation DOM render invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel); } while (nextEffect ! == null);Copy the code

Look at the main work of commitMutationEffects:

function commitMutationEffects(root, renderPriorityLevel) { // TODO: Should probably move the bulk of this function to commitWork. while (nextEffect ! == null) {// Iterate through the EffectList setCurrentFiber(nextEffect); Var flags = nexteect. Flags; If (flags & ContentReset) {... } // Update ref if (flags & ref) {... } var primaryFlags = flags & (Placement | Update | Deletion | Hydrating); Switch (primaryFlags) {case Placement: // Insert dom {... } Case PlacementAndUpdate: // Insert the DOM and update the DOM {// Placement commitPlacement(nextEffect); nextEffect.flags &= ~Placement; // Update var _current = nextEffect.alternate; commitWork(_current, nextEffect); break; } case Hydrating: //SSR {... } case HydratingAndUpdate: // SSR {... } Case Update: // Update dom {... } case Deletion: // delete the dom {... } } resetCurrentFiber(); nextEffect = nextEffect.nextEffect; }}Copy the code

In our sample code, PlacementAndUpdate is done, starting with the commitPlacement(nextEffect) method, and finally inserting our generated DOM tree into the rootDOM node after a series of decisions.

function appendChildToContainer(container, child) { var parentNode; if (container.nodeType === COMMENT_NODE) { parentNode = container.parentNode; parentNode.insertBefore(child, container); } else { parentNode = container; parentNode.appendChild(child); // Insert the entire DOM directly into root as a child node}}Copy the code

At this point, the code is actually rendered on the page. The following commitWork method executes the useLayoutEffect() function. The following commitWork method executes the effect unmount function of the last update.

Fiber tree switch

Before we get to the Layout phase, let’s look at this line of code

Current = finishedWork // Change the 'workInProgress' fiber tree to the 'current' treeCopy the code

This line of code is between the mutation and Layout phases. In the mutation phase, the CurrentFiber tree still points to the fiber tree before the update, so the DOM acquired in the life cycle hook is the DOM before the update. Hooks like componentDidMount and compentDidUpdate are executed in the Layout phase, so you can get the updated DOM to operate on.

layout

Briefly describe the work of the Layout phase:

  • Invoke lifecycle or hook-related operations
  • Assignment ref

As in the previous mutation phase, the effectList is traversed and the commitLayoutEffects method is executed.

Do {// call life cycle and hook related operations, assign value ref invokeGuardedCallback(null, commitLayoutEffects, NULL, root, lanes); } while (nextEffect ! == null);Copy the code

Look at the commitLayoutEffects method:

function commitLayoutEffects(root, committedLanes) { while (nextEffect ! == null) { setCurrentFiber(nextEffect); var flags = nextEffect.flags; / / life cycle or hook function called the if (flags & (Update | Callback)) {var current = nextEffect. Alternate; commitLifeCycles(root, current, nextEffect); } {// Get the DOM instance and update ref if (flags & ref) {commitAttachRef(nextEffect); } } resetCurrentFiber(); nextEffect = nextEffect.nextEffect; }}Copy the code

By the way, the callback to useLayoutEffect() is executed in the commitLifeCycles method, and the callback to useEffect() is scheduled in the schedulePassiveEffects method of the commitLifeCycles. The difference between useLayoutEffect() and useEffect() is shown here:

  • useLayoutEffecttheThe destruction function was updated last timeinmutationPhase destruction,Update the callback function this timeThis is after dom renderinglayoutPhase synchronous execution;
  • useEffectinBefore the mutationPhase will create scheduling tasks inlayoutPhase will add the destruction function and callback function topendingPassiveHookEffectsUnmountandpendingPassiveHookEffectsMountQueue, and finally itsThe destruction function was updated last timeandUpdate the callback function this timeAre all inlayoutPost-phase asynchronous execution;

To be clear, none of their updates will block DOM rendering.

After the layout

Remember those lines of code from the pre-mutation phase?

ScheduleCallback (NormalPriority$1, function () {flushPassiveEffects(); return null; });Copy the code

In this case, useEffect() is scheduled. The callback function is executed after the Layout phase, which handles useEffect’s last update destruct function and this update callback function.

conclusion

After reading this article, we can understand the following questions:

  1. What is the React rendering process?
  2. What did beginWork at React do?
  3. What did the React completeWork do?
  4. What did the React commitWork do?
  5. What is the difference between useEffect and useLayoutEffect?
  6. UseEffect and useLayoutEffect destroy function and update callback call timing?

Article series arrangement:

  1. React Fiber;
  2. React source series 2: React rendering mechanism;
  3. React source series 3: hooks useState, useReducer;
  4. React code series 4: hooks useEffect;
  5. React code Series 5: Hooks useCallback, useMemo;
  6. React source Series 6: Hooks useContext;
  7. React source series 7: React synthesis events;
  8. React source series eight: React diff algorithm;
  9. React source series 9: React update mechanism;
  10. React source series 10: Concurrent Mode;

Reference:

React official documents;

Making;