preface

The purpose of this paper is to clarify the complex function call relationship in the source code as far as possible, specify the core API or internal function logic, understand the React design idea as a whole, it is better to have a little understanding of the source code before reading (at least once to debug, know several symbolic functions in the source code, such as workLoop, beginWork, CompleteWork, etc.).

The React 16.8.6 source code for this article is React 16.8.6.

The article involved in the content of the complex, continuous update, welcome erratum or collection;

React 16.8.6 is based on four core libraries — React, React-DOM, React-Reconciler, and Scheduler, among which:

  • reactProvides the ability to normalize components into objects with similar specifications;
  • react-reconcilerProvides the ability to reconcile component updates, called renderer;
  • react-domTo achieve thereact-reconcilerThe Commit phase is defined to update some of the apis of the browser DOM and encapsulate some of the React-Reconciler apis;
  • schedulerProvides the ability of task scheduling;

1 React

1.1 the React Element

CreateElement is the React APP entry. JSX elements are automatically modified by Babel or some other translation library to call createElement, which returns an object called React Element. Has some special properties:

  • $$typeof
  • props
  • key
  • type
  • children
  • ref

$$typeof is a Symbol value that uniquely identifies a React Element. It can be used to identify whether a React Element is forged by XSS. Type is the type of the component. The value can be ClassComponent, FunctionComponent, or string (native H5 element). All other attributes are components with the same name;

When initializing the props property, createElement also filters the react reserved properties such as key, ref;

1.2 Component & PureComponent

The first thing to write a ClassComponent is to extend Component with the extends keyword. Component is a simple constructor that initializes properties or stereotype methods like this:

  • this.props
  • this.context
  • this.refs
  • this.updater
  • Component.prototype.setState
  • Component.prototype.forceUpdate

Refs is initialized as an empty object, and other properties are directly assigned with parameters;

To this prototype. Updater. EnqueueSetState, enclosing updater. EnqueueForceUpdate encapsulation, in turn, corresponding setState, forceUpdate, These two methods are the entry points for ClassComponent to trigger updates;

PureComponent defines the same properties as Component, inheriting the Component prototype method through parasitic composite inheritance, and adding a property isPureReactComponent with a value of true to distinguish it from Component.

As for Component lifecycle methods, they are common methods with special execution timing. There is no Magic. They are methods defined on child Component instances or prototypes.

2 ReactDOM

A Render function is provided that calls the Renderer to render the React Element into the browser. The core logic is as follows:

  1. Call createContainer to create FiberRootNode;
  2. Call updateContainer and pass in FiberRootNode and the other parameters to begin reconciliating component updates;

CreateContainer and updateContainer are apis for the React-Reconciler, so the react-DOM encapsulates the react-Reconciler logic and simplifies operations.

FiberRootNode is a special Fiber node, whose current attribute points to HostRootFiber, and the stateNode attribute of HostRootFiber points to FiberRootNode. The two are referenced in a loop, and their meanings are understood according to the name: FiberRootNode is the root node of the Fiber tree. HostRoot specifically refers to the root component in React. HostRootFiber is also the Fiber node corresponding to the root component.

UpdateContainer is the entry to apply the initial Mount;

3 Reconciler

React library is the most complex class library in React. Most of the core logic is here, so the reading experience is not good. If you can’t bear it, you can skip the detailed analysis and go directly to the conclusion.

3.1 Fiber

The React-Reconcile process is divided into two phases: Render and Commit. Render calculates and records changes to the virtual DOM. Commit commits these changes to the browser DOM. React 16 Render and Commit were performed synchronously at one time. If some of the code was executed for too long, the JS engine thread would take too long to execute, and other threads in the Render process (event triggering, GUI rendering, etc.) would not have the opportunity to execute their tasks.

In order to avoid frequent DOM changes, the Commit phase is still executed synchronously at once, whereas the Render phase is pure JS computation. Consider splitting the computation into several parts, giving control of each part and giving other threads a chance to perform their own tasks (such as handling user interaction events). Other threads return the control to the JS engine thread after the execution, and then continue to perform the next part of the calculation, which is time sharding;

In order to achieve time fragmentation, tasks in the Render stage need to be divided according to a certain granularity. React Element is the granularity chosen by React. I think the reason is that each React Element needs to do similar things, which is convenient for algorithm design.

Therefore, the tasks to be done in the Render stage are divided into small tasks, namely Unit of Work. The Unit of Work has some attributes, which are connected with each other through Pointers to facilitate traversal, forming a tree structure, which is Fiber.

Fiber nodes have the following important properties:

  • return: points to its parent node;
  • child: points to its first child node;
  • sibling: points to its first adjacent sibling node;
  • tag: Identify the types of Fiber nodes. Different types of Fiber nodes have different reconcile logic.
  • stateNode: Points to its local state, ClassComponent corresponds to the component instance, HostRootComponent corresponds to FiberRoot, String/Number corresponds to DOM Text Node, and generally null in other cases.
  • type: refers to the React Element’s type property, which corresponds to the FunctionComponent’s reference to Fiber generated by FunctionComponent;
  • alternate: Snapshot of Fiber node at this location from last update;
  • updateQueue: Indicates a Queue composed of all update objects. For details about the Batched update, see Update Queue & Batched Update.
  • effectTag: is used to mark changes to the Fiber node, which will be used during the Commit phase to determine how to update the browser DOM. See Effect Tag for details.

The Fiber tree of the nodes currently being scheduled is always Render or ready for Commit, where each Fiber node being processed is called workInProgess, The node pointed to by the workInProgess. Alternate attribute is the Fiber node constructed in the last update, which is simply the snapshot of the Fiber node in the last update. The Diff algorithm is built on the snapshot before the node and the node.

In the figure below, Current represents the Fiber constructed in this update, Snapshot represents the Fiber constructed in the last update. The alternate attribute of the corresponding nodes of the two trees points to the reference of each other node.

flowchart LR
    subgraph current[Current]
        rootFiber[App] --> div[div];
        div --> p[p];
        div -.X.-> span[span];
        div --> button[button];
    end
    subgraph snapshot[Snapshot]
        rootFiberA[App] --> divA[div];
        divA --> spanA[span];
        divA --> buttonA[button];
    end
    current <--alternate--> snapshot;

style p fill:#009933,color: #fff;
style span stroke-dasharray: 5 5,fill:#FF6666;

FiberRoot is the root node of the Fiber tree, but its properties are different from those of the Fiber node corresponding to the above components. FiberRoot can be understood as the Fiber node corresponding to the entire React App. Its important properties are as follows:

  • current: point to RootFiber;
  • containerInfo: points to the mount node of the React APP in the browser DOM.
  • finishedWorkAfter the Render phase, before the Commit phase, this attribute is assigned to the built Fiber root node. The resolved Commit phase updates the browser DOM from here.

3.2 defense XSS

In addition to React Element, when a subcomponent of type string/number is concatenated, the corresponding Fiber node is generated. Its tag attribute is HostText, which is a Symbol value. The Work Loop calls Document. createTextNode, which builds a DOM Node from the Fiber Node and inserts it into the Container.

Document. createTextNode escapes the text passed into the container, resulting in a DOM Node that can be safely inserted into the container via Document. appendChild. These are the two main things React does to defend against XSS;

3.3 Mount & Update

Mounting components is implemented via the Render API of the React-DOM, while updating components can be done in several ways:

  • ClassComponent: updates components with setState, forceUpdate (updater’s method enqueueSetState, enqueueForceUpdate is actually called);
  • FunctionComponent: Updates components via React Hook;

React Hook implementation is quite complex, and may be written separately in the future. The React-Reconciler calls the FunctionComponent through renderWithHook, which maintains local variables (closures) outside the function, For example, the local variable componentUpdateQueue maintains a linked list lastEffect, which stores useEffect Effect Callback, Clear Effect callback, and dependencies, etc. These local variables will be assigned to the corresponding properties of the FunctionComponent’s result in renderWithHook, and will then be handled similarly to ClassComponent.

Reactdom. render mentioned above, which calls createContainer to get APP’s FiberRoot (APP is also a component) and RootFiber for the root component; With the first unit of work (RootFiber) in place, it’s time to use updateContainer to fetch the next unit of work while executing it.

UpdateContainer contains all the core logic for harmonic component updates, almost identical to enqueueSetState and enqueueForceUpdate. The key to understanding component Mount & Update is to understand these three apis;

3.4 updateContainer & enqueueSetState & enqueueForceUpdate

The overall process is roughly as follows:

  1. Obtain Fiber node;
  2. Get expirationTime (see the expirationTime model below);
  3. Create and initialize the Update Object (see Update Object & Updater below);
  4. Enqueue the update (see update Queue below);
  5. Starting from Fiber node, start task scheduling (see Schedule Work below for details);

UpdateContainer unique logic:

  • No.1The Fiber node in the createContainer, RootFiber;
  • No.3Payload is the update object in the{element};

EnqueueSetState specific logic:

  • No.1Is the Fiber node corresponding to the component itself (an attribute added when the component is instantiated)_reactInternalFiber, pointing to the currentworkInProgess, which is the corresponding Fiber node of the component);
  • No.3Payload is set topartialState, the first parameter of setState;

EnqueueForceUpdate specific logic:

  • No.1withenqueueSetState;
  • No.3Properties will be settagForceUpdate, to indicate that this is a forced update;

3.5 ExpirationTime model

3.1 mentioned the concept of task scheduling, that is, each unit of work may have different priorities. In the scheduling process, if a new task with a high priority appears, the execution of a task with a low priority should be interrupted. React introduced the ExpirationTime model to express the high priority. Fiber nodes and Update objects both have a expirationTime property that represents the priority of each node.

Directly to the conclusion:

  • In the React-Reconciler, the bigger the expirationTime, the higher the priority;
  • In scheduler, the larger the expirationTime is, the lower the priority is;

The calculation rules are complex and can be skipped, but it must be mentioned that they are key to understanding why the React-Reconciler is in opposition to scheduler’s methods of judgment.

3.5.1 track of Unit of ExpirationTime

Context context Time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context time = context Convert a value in milliseconds to a value in milliseconds.

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
const MAX_SIGNED_31_BIT_INT = 1073741823;

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;

// 1 unit of expiration time represents 10ms.
function msToExpirationTime(ms: number) :ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

function expirationTimeToMs(expirationTime: ExpirationTime) :number {
  return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}
Copy the code

Source code (ms/UNIT_SIZE) means the value in milliseconds divided by 10, becomes a value in 10ms units, that is, ExpirationTime;

Bitwise or | 0 are in order to take down the whole (ES), and above all ((ms/UNIT_SIZE) | 0) means to 10 ms as the unit value of the integer part, in order to facilitate understanding, I call it ExpirationTimeInt;

ExpirationTimeInt = ExpirationTimeInt = ExpirationTimeInt = ExpirationTimeInt = ExpirationTimeInt = ExpirationTimeInt And then we look down.

The NoWork reconciler is a variable with a value of 0, which means that in the React-Reconciler context time = 0 has special meaning, and the ExpirationTimeInt context time = 0. This represents the same meaning as NoWork, which obviously makes NoWork’s judgment more complicated. In order to avoid conflicts with NoWork, an offset is added, and there is a potential problem with addition. The operation sum may lose precision because it is larger than number.max_safe_INTEGER.

To avoid losing precision, React would do MAGIC_NUMBER_OFFSET -expirationTimeInt, MAGIC_NUMBER_OFFSET = Number. Max_safe_integer-1 in V8 for 32-bit systems The special variable Sync has a value of number.max_safe_INTEGER;

Because the ExpirationTimeInt must be a positive integer, it must be accurate unless it itself exceeds Number.max_SAFE_INTEGER, and msToExpirationTime is called in the React-Reconciler, MAGIC_NUMBER_OFFSET – ExpirationTimeInt = -1 if the value is set to Sync, then MAGIC_NUMBER_OFFSET – ExpirationTimeInt = -1 if the value is set to Sync. In addition, the maximum value of the parameter MS mentioned earlier, with the qualifier “general case”, what about the unusual case?

function recomputeCurrentRendererTime() {
  const currentTimeMs = now() - originalStartTimeMs;
  currentRendererTime = msToExpirationTime(currentTimeMs);
}
Copy the code

Now is the scheduler of an API, is actually the Performance. The prototype. Now, when the browser is not compatible with the fallback to Date. The prototype. Now;

OriginalStartTimeMs is a time interval in ms obtained by now, which will not be changed later, and can be regarded as a constant. Therefore, currentTimeMs is positively correlated with time. It can be calculated that React APP has been running for more than 12 days continuously. CurrentTimeMs will be greater than number. MAX_SAFE_INTEGER and the calculation will lose precision, but this is almost never the case;

Finally, MS is transformed into expirationTime, and in the React-Reconciler context Reconciler involves the priority judgment of expirationTime, not ExpirationTimeInt. To sum up, we get the final rule:

The smaller your ExpirationTimeInt is, the larger your expirationTime is and the higher the priority is.

MsToExpirationTime context = context timetoms context = Context Timetoms context = Context timetoms context = Context timetoms context

3.5.2 Calculation rules

Source code comments are complete, directly paste the source code (with the main context variable explanation) :

  • currentTime: Through the above mentionednowGet time in ms, incomingcomputeExpirationForFiberIt was converted to time in 10ms;
  • expirationContext: is a local variable calledsyncUpdatesordeferredUpdatesWill change its value, its initial value is NoWork;
  • isWorking: is a local variable set to true on Render or Commit and false on end;
  • isCommitting: is a local variable, set to true on Commit and false on end;
  • fiber.mode: fiber.mode is also defined, validated, and added by the bit operationConcurrentMode;
  • isBatchingInteractiveUpdates: is a local variable calleddeferredUpdatesorinteractiveUpdatesWill change its value, its initial value is false;

React 16.8.6, lines 1595;

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if(expirationContext ! == NoWork) {// An explicit expiration context was set;
    expirationTime = expirationContext;
  } else if (isWorking) {
    if (isCommitting) {
      // Updates that occur during the commit phase should have sync priority
      // by default.
      expirationTime = Sync;
    } else {
      // Updates during the render phase should expire at the same time as
      // the work that is being rendered.expirationTime = nextRenderExpirationTime; }}else {
    // No explicit expiration context was set, and we're not currently
    // performing work. Calculate a new expiration time.
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // If we're in the middle of rendering a tree, do not update at the same
      // expiration time that is already rendering.
      if(nextRoot ! = =null && expirationTime === nextRenderExpirationTime) {
        expirationTime -= 1; }}else {
      // This is a sync updateexpirationTime = Sync; }}// expirationTime = expirationTime = expirationTime
  if (isBatchingInteractiveUpdates) {
    // This is an interactive update. Keep track of the lowest pending
    // interactive expiration time. This allows us to synchronously flush
    // all interactive updates when needed.
    if( lowestPriorityPendingInteractiveExpirationTime === NoWork || expirationTime < lowestPriorityPendingInteractiveExpirationTime ) { lowestPriorityPendingInteractiveExpirationTime = expirationTime; }}return expirationTime;
}
Copy the code

Don’t get bogged down in code details, just understand the comments and understand the flow, which is as follows:

Graph TB start[input currentTime,fiber] --> if1{expirationContext} if1 --Yes--> return1[return expirationContext]; If1 --No--> if2{Render or Commit}; If2 --Yes--> if3{Commit phase}; if3 --Yes--> return2[return Sync]; if3 --No--> return3[return nextRenderExpirationTime]; If2 --Yes--> if4{mode = ConcurrentMode}; If4 --Yes--> if5{update triggered by user interaction}; if5 --Yes--> compute1[result = computeInteractiveExpiration]; if5 --No--> compute2[result = computeAsyncExpiration]; Compute1 --> if6{result has the same expiration time as the next node to be reconciled}; compute2 --> if6; if6 --Yes--> return4[return result - 1]; if4 --No--> return5[return Sync];

Including isBatchingInteractiveUpdates is more special, used to determine whether to update the interactive event trigger, is call the computeInteractiveExpiration, otherwise call computeAsyncExpiration, Its parameters currentTime is actually a scheduler APInow execution, actual Performance is call. Prototype. Now, there is compatibility is back up to Date. The prototype. Now

ComputeInteractiveExpiration and computeAsyncExpiration is computeExpirationBucket encapsulation, related to the source code is as follows:

// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
//
// If the main thread is being blocked so long that you hit the expiration,
// it's a problem that could be solved with better scheduling.
//
// People will be more likely to notice this and fix it with the long
// expiration time in development.
//
// In production we opt for better UX at the risk of masking scheduling
// problems, by expiring fast.
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
// the names to reflect.
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
) :ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}
// If precision = 25,
// num from 125 to 149 is 150; num from 150 to 174 is 175
function ceiling(num: number, precision: number) :number {
  return (((num / precision) | 0) + 1) * precision;
}
// This function contains the calculation logic of ExpirationTime
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs,) :ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
Copy the code

It looks complicated, but most of the details have been covered above. Here the effect of the ceiling is to ignore the difference between the currentTime passed in at different points in time, so that the expirationTime within a given interval is exactly the same, with the same priority.

BucketSizeMs/UNIT_SIZE is the specified time interval (10ms), bucketSizeMs is a ms unit of time, depending on the update method to determine the value;

ExpirationInMs is a context value with a higher priority.

Magic_number_offset-currenttime + expirationInMs/UNIT_SIZE

  • According to the source, the only variable iscurrentTime, other constants, andcurrentTimeThe unit of is the aforementioned 10ms, so there is no need to divideUNIT_SIZE;
  • expirationInMs / UNIT_SIZERepresents a weight value in the unit of 10 ms, used to distinguish the priority of interactive event-triggered update from that of ordinary asynchronous update;
  • MAGIC_NUMBER_OFFSET - x, which means to add an offset to x;

Context magic_number_offset-currentTime = context = context = context = context = context = context = context = context = context = context = context Multiple computations within bucketSizeMsms yield the same result, which allows similar Updates to be consolidated into a single update, called Batched Updates.

It is worth mentioning that the meaning of Bucket, to help understand this algorithm, is to throw different intervals of time into different buckets, for the same Bucket, out of the same thing is exactly the same.

3.5.3 summary

At this point, four priorities appear, in descending order from highest to lowest priority:

  • Sync: Constant, the value isNumber.MAX_SAFE_INTEGER“, meaning immediate execution;
  • HighPriority: value is offset – current time + 50 (15 in production environment);
  • LowPriority: value is offset – current time + 500;
  • NoWork: constant, with a value of 0, meaning no scheduling is required;

Updates triggered by interactive events have a higher priority than regular asynchronous updates (fiber is all regular asynchronous updates by default).

  • currentTimeThe greater the,MAGIC_NUMBER_OFFSET - currentTimeThe smaller,ceiling(...)The smaller,Magic_number_offset-ceiling (...)Larger (that is, over time, the calculation results represent a higher priority);
  • The greater the priority weight value,ceiling(...)The greater the,Magic_number_offset-ceiling (...)Smaller (that is, the larger the weight value, the smaller the priority);

3.6 Update Object & Updater

Updater was mentioned in 1.2 Component above. The prototype setState and forceUpdate methods encapsulate updater’s methods. The Updater is injected by the React-Reconciler (step 3 in the Work Loop below).

Updater is an object with the following core methods:

  • enqueueSetState: is encapsulated by setState and used to trigger updates;
  • enqueueForceUpdate: is encapsulated by forceUpdate and used to trigger updates;

The logic of the two methods is almost the same: an object called Update is generated, which is used to progressively calculate state during the Render phase, and update contains the following important properties:

  • expirationTime: Expiration time, used to determine the priority.
  • tag: An update is used to distinguish different meanings. For example, ForceUpdate indicates a forcible update.
  • payloadFor example, the value of updateContainer is the React Element, setState is the first parameter, and forceUpdate is null.
  • callback: the callback triggered after the update processing is complete;
  • next: points to the next update object in its UpdateQueue;

The Update object is initialized after creation and then added to the end of the updateQueue queue in the Fiber node;

3.7 Update Queue & Batched Updates

If setState and forceUpdate are called continuously, then the efficiency is obviously very low. In order to optimize the efficiency, it is necessary to merge setState and forceUpdate together. So React-Reconciler came up with the Update Queue model.

The Update Queue is a Queue implemented by a linked list and contains the following key attributes:

  • baseStateThe result of the current update (resultState) will be used as the basis for the calculation of the next update. This is to support the passing of callback to setState, in which the result of the previous update can be obtained.
  • firstUpdate: Queue header;
  • lastUpdate: queue tail;

How does React determine which updates need to merge into an update Ue and which don’t? Use of setState and forceUpdate in React scenarios is very complicated, and it is difficult to judge by React. It is better to provide a unified interface through which to call setState and forceUpdate, and merge the created updates into the same updateQueue. This interface is called batchedUpdates, which encapsulates the logic for successive calls to setState and forceUpdate in a callback and then passes the callback to batchedUpdates.

BatchedUpdates maintain a local variable isBatchingUpdates inside the React-Reconciler, and executing batchedUpdates sets this variable to true, and then executes callback, The setState and forceUpdate functions in callback Perform Work logic every time they are executed (Render + Commit, see below), but since isBatchingUpdates are true, Perform Work (Perform Work); Perform Perform Work when setState, forceUpdate, and updateQueue are executed Restore isBatchingUpdates;

UnbatchedUpdates, on the other hand, maintain an isUnbatchingUpdates, which are the opposite of isBatchingUpdates. When used together, the isBatchingUpdates will affect each other. To facilitate understanding, post a code (theoretical effect, to be verified) :

batchedUpdates(() = >{
    this.setState(...) ;// Perform no Perform Work, update only;
    this.setState(...) ;// Perform no Perform Work, update only;
    
    unbatchedUpdates(() = >{
        this.setState(...) ;// Perform Work;
        this.setState(...) ;// Perform Work;
    });
    
    this.setState(...) ;// Perform no Perform Work, update only;
    
}); // Perform Work;
Copy the code

Last but not least, event callbacks defined by the React synthesis event will automatically call batchedUpdates. Successive calls to setState and forceUpdate will be merged into event callbacks.

3.8 Effect of the Tag

In order to reduce the number of browser DOM operations, the React-Reconciler uses Effect tags to identify the changes that have occurred in a Fiber, which are derived from the Diff algorithm (see below) during the Render phase, The collection is then collectively committed to the browser DOM during the Commit phase.

An Effect Tag defines an Effect type and operation using a bit operation:

  • Define Tag: the value of Tag is defined as 2n(n>=0); 2 ^ n (n > = 0); 2 n (n > = 0); For each new Effect Tag, n increases by 1, i.e. 0,1,2,4,8… ;
  • Bitwise or: adds an effect tag, for exampleeffectTag |= UpdateAdd the Update tag.
  • Bitwise and: verify effect tag,effectTag & UpdateIf the value is 0, the Update tag is not included.
  • Reverse bitwise: collocates bitwise and, removes effect tag, for exampleeffectTag &= ~UpdateRemove the Update tag;

3.9 the Schedule Work

Here is the entry to task scheduling, and the following is obtained from the process in Section 3.4 above:

  • Nodes to be scheduledfiber(Mount to RootFiber; SetState/forceUpdateinstanceOfComponent._reactInternalFiber);
  • The expiration date of this updateexpirationTimeIf thefiber.expirationTimeLess thanexpirationTime, indicating that the priority is not high, and the current scheduling will be skipped.
  • Batched Updates for this schedulefiber.updateQueue;

Then start the Schedule Work process:

  1. updatefiberAll expiration time-related attributes of the
  2. Interrupt low-priority tasks;
  3. Request Work;

Mainly in step 1 is updated fiber. ExpirationTime and fiber childExpirationTime, is that if the original attribute values is less than expirationTime, illustrate the priority of this update makes the fiber increase, If fiber is not a RootFiber (such as setState updates), then fiber. Return will iterate over its parent. Update its parent childExpirationTime, this ensures that a bit, namely fiebr. ChildExpirationTime represents the largest expiration time in the offspring of fiber node;

In addition to the above attributes, there are several expiration attributes that are updated in Step 1:

  • earliestPendingTime: The earliest expiration time, note that the expiration time also has a negative offset, so it actually means that the expiration time with the largest value has the highest priority;
  • latestPendingTime: The latest expiration time, note that the expiration time also has a negative offset, so it actually means that the expiration time with the smallest value has the lowest priority;
  • nextExpirationTimeToWorkOn: according toearliestPendingTimePing, Suspend, Suspend, Suspend, Suspend, Suspend, Suspend, Suspend, Suspend, Suspend

Context Context = context context = context context = context = context = context = context = context = context = context = context In the renderRoot assignment for root. NextExpirationTimeToWorkOn), will break the execution of the next update, and reset the Value Stack;

The Value Stack is used to store the values of providers at different levels during the React Context reconciliation process. Each layer of Provider reconciliation is pushed to the top of the Stack, and the Value of the component that subscribes to the Value of the Provider is added to the Stack. This ensures that the component can get the value of the nearest Provider, and when the Provider’s child components are reconciled, their value is pushed off the stack, ensuring that the sibling component that is reconciled later gets the correct value, and so on.

3.10 the Request Work

Overall process:

  1. Update RootFibernextScheduledRoot;
  2. Batched Updates (direct return) or Unbatched Updates (performWorkOnRoot);
  3. If this update expiresexpirationTimeA value ofSync, the synchronous update logic is performedperformSyncWorkOtherwise, asynchronous update logic is performedscheduleCallbackWithExpirationTime;

NextScheduledRoot consists of a linked list structure, each node corresponds to the RootFiber of each update, in addition to the local variable firstScheduledRoot, lastScheduledRoot points to the RootFiber of the first and last update respectively. The main logic of Step 1 is to maintain such a linked list structure and the pointing of the first and last Pointers, which will be used later.

The logic in step 2 is described above, and whether to merge into the same update uE depends on the value of the local variable. It is worth to note that when Unbatched updates are executed, performWorkOnRoot is used to process the update separately. Don’t merge with other updates, so just reconcile a single RootFiber, which I’ll call [sync single Root] for easy memorization, not to be confused with other similarly named functions;

Batched Updates directly return, since batchedUpdates provide a special implementation that calls setState and forceUpdate in the fn callback to trigger the Update logic, and then creates the Update object. Obtain the Fiber node Fiber from the component instance, enqueue the Update object into Fiber. UpdateQueue, and then terminate the Request Work logic directly. Fiber from the component instance is the same node. Update objects created after the component instance are collected in the same queue. PerformSyncWork is performed after all Update objects are collected. The source code is as follows:

function batchedUpdates<A.R> (fn: (a: A) => R, a: A) :R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    // Collect update objects into update Ue
    return fn(a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if(! isBatchingUpdates && ! isRendering) {// Synchronize all update objects at once;performSyncWork(); }}}Copy the code

Step 3 is divided into two cases, synchronous update and asynchronous update. The judgment method is based on whether the expiration time of this update is equal to Sync. Synchronous update means that all tasks are executed at one time, while asynchronous update means the task fragmentation execution mentioned above.

The update logic here means that all update corresponding RootFiber will be processed in descending order of priority from high to low, so their corresponding core functions are named performSyncWork and performAsyncWork respectively. For convenience of memory, They are called synchronous multi-root and asynchronous multi-root.

3.10.1 Synchronizing Multiple Root UsersperformSyncWork

Synchronizing multiple Root is the encapsulation of performWorkOnRoot, that is, synchronizing multiple updated corresponding multiple RootFiber.

In fact, the nextScheduledRoot linked list composed of the RootFiber nodes mentioned above will be traversed. Each iteration will set the RootFiber with the highest priority as nextFlushedRoot. One of the biggest ExpirationTime set as nextFlushedExpirationTime, and call the nextFlushedRoot performWorkOnRoot processing, namely to reconcile the highest priority a RootFiber;

After traversal, means all RootFiber has been to reconcile and Commit, but if nextFlushedExpirationTime! = = NoWork, means the RootFiber has been mixed but has not been a Commit, if there is no residual current time divided time will appear this kind of situation (see below), then call scheduleCallbackWithExpirationTime, The scheduler handles when the Commit logic executes, and the scheduler executes the Commit logic later in a time slice that has time to spare.

3.10.2 Asynchronous Multiple RootscheduleCallbackWithExpirationTime

Asynchronous multi-root is the encapsulation of performAsyncWork, that is, to harmonize multiple rootfibers corresponding to multiple updates in an asynchronous manner. Asynchronous refers to scheduling tasks in the way of time fragmentation mentioned above. Such task scheduling capability is realized by the scheduler in the following.

PerformAsyncWork is also a wrapper around performWorkOnRoot;

Updated…

3.10.3 Synchronizing a Single RootperformWorkOnRoot

Synchronize single Root, i.e. start with a single updated RootFiber, listing the parameters to help understand:

  • rootThat RootFiber;
  • expirationTime: Expiration time of this update;
  • isYieldy: Whether it can be interrupted. Synchronous updates cannot be interrupted, but asynchronous updates can be interrupted.

The same and asynchronous update process is almost the same as follows:

  1. ifroot.finishedWorkIf yes, the Render phase has been executedcompleteRootStart Commit, after which the process ends;
  2. Otherwise, it is not Render good, executerenderRootBegan to Render;
  3. Judge againroot.finishedWorkIf yes, Render is done, executecompleteRootStart Commit, after which the process ends;

The only difference between the two logic is that in step3, for asynchronous updates, shouldYield is called to determine if there is any time left before executing the completeRoot. ShouldYield is an API provided by scheduler, It is used to determine whether the current time sharding has left time to continue executing the task. CompleteRoot will be executed only when there is left time. Otherwise, the finishedWork attribute of RootFiber will be updated and Commit will be performed after the time sharding is completed.

The overall flow chart is as follows:

graph TB start(Start) ; return(End); start--> isYieldy; isYieldy{! isYieldy} --Yes--> isFinished1{root.finishedWork ! == null}; isFinished1 --Yes--> completeRoot1[completeRoot]; isFinished1 --No--> renderRoot1[renderRoot]; renderRoot1 --> isFinished11{root.finishedWork ! == null}; isFinished11 --Yes--> completeRoot1; isFinished11 --No--> return; completeRoot1 --> return; isYieldy --No--> isFinished2{root.finishedWork ! == null}; isFinished2 --Yes--> completeRoot2[completeRoot]; isFinished2 --No--> renderRoot2[renderRoot]; renderRoot2 --> isFinished21{root.finishedWork ! == null}; isFinished21 --Yes--> shouldYield{"! shouldYield()"}; shouldYield --Yes--> completeRoot2; shouldYield --No--> return; isFinished21 --No--> return; completeRoot2 --> return;

3.10.4 renderWork & completeWork

RenderWork is responsible for building the entire Fiber from RootFiber and collecting Effect tags. CompleteWork calls the ReactDOM API to update the browser DOM based on the collected Effect tags.

These two functions trigger the lifecycle functions of the corresponding phases of the ClassComponent, or the FunctionComponent hooks;

RenderWork process:

  1. Update Passive Effects (useEffect);
  2. If it’s a fresh start update, reset the Value Stack to update local variablesnextRenderExpirationTime = root.nextExpirationTimeToWorkOn, and initialize itworkInProgress, set local variablesnextUnitOfWork = workInProgress;
  3. Enter the Work Loop and harmonize Fiber node formally;

root.nextExpirationTimeToWorkOn ! == nextRenderExpirationTime || root ! = = nextRoot | | nextUnitOfWork = = = null as a brand new start update, namely the expiration time is different, different RootFiber, nextUnitOfWork as initial value, these three conditions are updated as a new beginning.

Work can be understood as a harmonizing component or a harmonizing Fiber node. WorkInProgress is the Fiber node being harmonized, and nextUnitOfWork is the next Fiber node to be harmonized.

3.11 Work Loop

Perform one unit of Work each time and perform the next unit of Work in the order in which it performs. This is the Work Loop. Perform one unit of Work each time and perform the next unit of Work in the order in which it performs.

  1. A variable p refers to the root node (RootFiber);
  2. Get Component (Component reference or instance based on its stateNode or type);
  3. Get the React Element of the child Component (either by executing the Render method of Component, or by executing Component directly);
  4. Create newFiber from React Element and make its return point to parent p (Diff algorithm in this process);
  5. If newFiber is null, the fiber p points to corresponds to the last React Element, i.e. it is a leaf nodestep 6Otherwise, p points to newFiber and retrystep 1(Iterate over the child nodes);
  6. Make p.r eturn. FirstEffect. NextEffect at p.e ffectTag (pointed to p effectTag on fiber connected to the parent node effect chain table tail);
  7. If p.sibling exists, p points to p.sibling and executes againstep 1(Traverse the adjacent sibling node), otherwise continue down;
  8. If p.turn exists, p points to p.turn and is executed againstep 6(Go back to the parent node), otherwise, it means backtracking to RootFiber and the process ends;

3.11.1 beginWork

The main logic of Step 3 is to trigger the lifecycle method and get the child components.

3.11.2 completeWork

3.11 the Commit

3.12 the Diff

Step 4 actually contains many details in the above process. The main logic is to use the Diff algorithm to compare Fiber and fiber. Alternate to get the change of fiber.

4 Scheduler

The thread-level scheduling capability mentioned above of dividing a computation into parts and giving other threads a chance to perform their own tasks as they execute each part can be implemented using a native JS API called requestIdleCallback, but due to some disadvantages of this function, The React team implemented a similar mechanism directly, encapsulated in Scheduler. It’s worth noting that Scheduler isn’t exactly a Polyfill of requestIdleCallback. The difference between scheduler and requestIdleCallback is significant. Scheduler is the Enhancer of requestIdleCallback;

Scheduler is based on requestAnimationFrame and MessageChanel;

4.1 requestIdleCallback

4.2 requestAnimationFrame

4.3 MessageChanel

4.4 summarize