It has been a year and a half since I switched to React. After familiar with its usage, can not avoid to in-depth understanding of its implementation principle, the Internet related source analysis of the article is quite a lot, but the total feeling is not as profound as their reading understanding. So I spent a few weekends to understand the common process. It is also through this article that I share my personal understanding.

Before the specific source process analysis, according to personal understanding, combined with better online articles, first to analyze some conceptual things. Then analyze the specific process logic.

React 15

Architectural layering

React 15 (before Fiber) The entire rendering process is divided into two parts:

  • Reconciler(coordinator); Responsible for identifying changing components
  • Renderer(renderer); Responsible for rendering the changing components onto the page

Reconciler

React can trigger updates with setState, forceUpdate, and reactdom.render. Every time a renewal occurs, the Reconciler does the following:

  1. Calling componentrenderMethod that will be returnedJSXConvert to virtualDOM
  2. The virtualDOMAnd virtual from the last updateDOMcontrast
  3. Find out the virtual changes in this update by comparisonDOM
  4. noticeRendererRender the changing virtual DOM onto the page

Renderer

After the Reconciler is played on an updated node, the Renderer is notified to render/update the node according to the different “host environment.”

React 15’s flaws

The React 15 diff process performs updates recursively. Because it’s recursive, once it starts, it’s “unstoppable.” When the hierarchy is too deep or the diff logic (the logic in the hook function) is too complex, causing the recursive update to take too long and the Js thread to stay stuck, the user interaction and rendering can get stuck. Look at an example: count-demo

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>4<li>
<li>3<li>        ->       <li>6<li>
Copy the code

When the button is clicked, the list changes from 1, 2 and 3 on the left to 2, 4 and 6 on the right. Updates on each node are basically synchronous to the user, but they are actually traversed sequentially. The specific steps are as follows:

  1. Click on thebutton, trigger the update
  2. Reconcilerdetected<li1>Need to change to<li2>, immediately notifyRendererupdateDOM. List theTwo, two, three
  3. Reconcilerdetected<li2>Need to change to<li4>To informRendererupdateDOM. List theTwo, four, three
  4. Reconcilerdetected<li3>Need to change to<li6>, immediately notifyRendererupdateDOM. List theTwo, four, six

It is then clear that the Reconciler and the Renderer work alternately, with the second node entering the Reconciler after the first node has changed on the page. Because the entire process is synchronous, all nodes are updated simultaneously from the user’s point of view. If you break the update, you will see a new node tree on the page that is not fully updated!

If step 3 and step 4 cannot proceed due to a sudden interruption of the current task during step 2, the user will see:

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>2<li>
<li>3<li>        ->       <li>3<li>
Copy the code

React definitely doesn’t want this to happen. But this application scenario is absolutely necessary. Imagine a user making an input event at a point in time when the content in the input should be updated, but the user’s input is delayed because of an update to a list that is not currently visible. The experience for the user is sluggish. So the React team needed to find a way to address this flaw.

React 16

Architectural layering

The Act15 architecture could not support asynchronous updates and required refactoring, so the Act16 architecture was split into three layers:

  • Scheduler b. Scheduling tasks are prioritized, and high-priority tasks are given priority into Reconciler
  • It is a Reconciler; Responsible for identifying changing components
  • Renderer; Responsible for rendering the changing components onto the page

Scheduler

React 16 requires Diff updates to be interruptible. React 15 requires Diff updates to be interruptible. React 16 requires Diff updates to be interruptible.

The React team uses cooperative scheduling, which involves active interrupts and controller transfers. The criterion is timeout detection. There is also a need for a mechanism to tell interrupted tasks when to resume/resume. React uses the browser’s requestIdleCallback interface to notify users when the browser has time left.

React abandoned rIdc for some reason, instead implementing its own, more fully functional Polyfill, or Scheduler. In addition to the ability to trigger callbacks when idle, Scheduler provides a variety of scheduling priorities for tasks to set.

Reconciler

In React 15, the Reconciler is dealt with the Virtual DOM recursively. Act16 uses a new data structure: Fiber. The Virtual DOM tree is changed from the previous top-down tree structure to a “graph” based on multi-directional linked lists.

The update process has gone from being recursive to a cycle that can be interrupted. Each time the loop calls shouldYield() to see if there is any time left. Source code address.

function workLoopConcurrent() {
    // Perform work until Scheduler asks us to yield
    while(workInProgress ! = =null&&! shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code

According to the previous analysis, the interruption of React 15 resulted in incomplete page updates because Reconciler and Renderer work alternately, so in React 16, Reconciler and Renderer no longer work alternately. When Scheduler hands Reconciler the task, the Reconciler simply marks the changed Virtual DOM with add, delete, and update, without notifies the Renderer to render. Something like this:

export const Placement = / * * / 0b0000000000010;
export const Update = / * * / 0b0000000000100;
export const PlacementAndUpdate = / * * / 0b0000000000110;
export const Deletion = / * * / 0b0000000001000;
Copy the code

Only when all components are finished with the Reconciler are the Reconciler uniformly handed over to the Renderer for rendering updates.

Renderer(Commit)

The Renderer synchronously performs the corresponding rendering operations based on the Reconciler’s markings for the Virtual DOM.

For the example we used in the previous section, the entire update process in the React 16 architecture looks like this:

  1. setStateGenerates an update with the following contents:state.countfrom1into2
  2. Update is givenScheduler.SchedulerFind no other higher priority and assign the taskReconciler
  3. ReconcilerGot a call. Start traversingVirtual DOMAnd judge which onesVirtual DOMNeed to update, for need to updateVirtual DOMbranding
  4. ReconcilerGo through all of themVirtual DOMTo informRenderer
  5. RendererAccording to theVirtual DOMTo perform the corresponding node operation

Steps 2, 3, and 4 May be interrupted at any time due to the following reasons:

  • There are other higher priority tasks that need to be updated first
  • The current frame has no time left

Because Scheduler and Reconciler work in memory and do not update the nodes on the page, users do not see pages that are not fully updated.

The principle of Diff

React Diff is based on three assumptions:

  • DOM is rarely moved across hierarchies, rightVirtual DOMTrees are compared hierarchically. Two trees are compared only on nodes of the same hierarchy.
  • The tree structure varies with different types of components. Components of the same type have similar tree structures
  • A group of child nodes at the same level can be updated, removed, or added. Nodes can be distinguished by their unique IDS

The React component, either in JSX format or created by React. CreateElement, will eventually be converted to the Virtual DOM, and the corresponding Virtual DOM tree will be generated based on the hierarchy. React 15 creates a new Virtual DOM each time it is updated, and then compares the differences between the old and new Virtual DOM recursively, resulting in a “patch update” that maps to the real DOM. The React 16 process will be analyzed later

Source code analysis

React source code is very large, and the source code has been adjusted since 2016. At present, the latest source code on Github is reserved xxx.new.js and xxx.old.js. React source code is managed by using Monorepo structure. Different functions are grouped into different packages. The only disadvantage may be that method address index is not very convenient. Before you start, check out this official reading guide

Because the source code is too much too complex, all I here as far as possible from the largest to small, from surface to point one analysis. The general process is as follows:

  1. The first thing to know is to passJSXorcreateElementWhat the coded code actually turns into
  2. Then analyze the entry point of the applicationReactDOM.render
  3. Further analysissetStateUpdate process
  4. Finally, specific analysisScheduler,Reconciler,RendererThe general process of

In addition to reactdom. render and setState, there are forceUpdate operations that trigger render updates. The main difference is that forceUpdate does not walk shouldComponentUpdate hook function.

The data structure

Fiber

Before starting the formal process analysis, I hope you have a certain understanding of Fiber. If not, I suggest you watch this video first. Then, familiarize yourself with the structure of the ReactFiber.

export type Fiber = {
    // Task type information;
    // Such as ClassComponent, FunctionComponent, ContextProvider
    tag: WorkTag,
    key: null | string,
    // The value of reactElement. Type, used for the reserved identifier during reconciliation.
    elementType: any,
    // fiber is associated with function/class
    type: any,
    // Any type!! This generally refers to the actual DOM node or instance of the corresponding component that Fiber corresponds to
    stateNode: any,
    // Parent node/parent component
    return: Fiber | null.// First child node
    child: Fiber | null.// Next sibling node
    sibling: Fiber | null.// Change the state, such as delete, move
    effectTag: SideEffectTag,
    // Used to link old and new trees; Old -> new, new -> old
    alternate: Fiber | null.// Development mode
    mode: TypeOfMode,
    // ...
  };
Copy the code

FiberRoot

Each tree or application rendered by reactdom.render initializes a corresponding FiberRoot object as the starting point for the application. Its data structure is ReactFiberRoot.

type BaseFiberRootProperties = {
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,
  // root, the second argument to reactdom.render ()
  containerInfo: any,
  // Persistent updates are used. React-dom is an entire application update, so you don't need this
  pendingChildren: any,
  // The Fiber object corresponding to the root node is currently applied
  current: Fiber,
  // The expiration time of the current update
  finishedExpirationTime: ExpirationTime,
  // The FiberRoot object that has completed the task will only process the task corresponding to this value during the COMMIT phase
  finishedWork: Fiber | null.// The oldest unexpired time in the tree
  firstPendingTime: ExpirationTime,
  // Suspends the next known due time in the task
  nextKnownPendingLevel: ExpirationTime,
  // The latest unexpired time in the tree
  lastPingedTime: ExpirationTime,
  // The latest expiration time
  lastExpiredTime: ExpirationTime,
  // ...
};
Copy the code

Fiber type

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Undefined type; It could be class or function
export const HostRoot = 3; / / the root of the tree
export const HostPortal = 4; // a subtree
export const HostComponent = 5; // native node; Depending on the environment, the browser environment is div, etc
export const HostText = 6; // Plain text node
export const Fragment = 7;
Copy the code

model

As of React 16.13.1, there are several built-in development modes:

export type TypeOfMode = number;
/ / normal mode | Legacy mode, synchronous rendering, React15-16 production environment
export const NoMode = 0b0000;
// Strict mode, used to detect the presence of deprecated APIS (which call the render phase lifecycle multiple times), used by the react16-17 development environment
export const StrictMode = 0b0001;
// ConcurrentMode transitional version of the mode
export const BlockingMode = 0b0010;
// Concurrent mode, asynchronous rendering, React17 production environment
export const ConcurrentMode = 0b0100;
// Performance test mode, used to detect where there are performance problems, react16-17 development environment use
export const ProfileMode = 0b1000;
Copy the code

This article examines only the ConcurrentMode mode

JSX and React. The createElement method

Let’s take a look at one of the simplest JSX encoded components. Here’s the code conversion with Babel. Here’s the code

// JSX
class App extends React.Component {
    render() {
        return <div />
    }
}

// babel
var App = /*#__PURE__*/function (_React$Component) {
    _inherits(App, _React$Component);

    var _super = _createSuper(App);

    function App() {
        _classCallCheck(this, App);

        return _super.apply(this, arguments);
    }

    _createClass(App, [{
        key: "render",
        value: function render() {
            return /*#__PURE__*/React.createElement("div", null);
        }
    }]);

    return App;
}(React.Component);
Copy the code

The key point is that the Render method actually calls the React. CreateElement method. So we just need to analyze what createElement does. Let’s look at the structure of ReactElement:

let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol= = ='function' && Symbol.for) {
    REACT_ELEMENT_TYPE = Symbol.for('react.element');
}

const ReactElement = function (type, key, ref, props) {
    const element = {
        // Uniquely identified as React Element to prevent XSS from storing symbols in JSON
        ?typeof: REACT_ELEMENT_TYPE,

        type: type,
        key: key,
        ref: ref,
        props: props,
    }
    return element;
}
Copy the code

A very simple data structure, each attribute is clear at a glance, not one explanation. React.createElement

Preventing XSS attacks

If you are not aware of XSS attacks, you are advised to read this article first how to prevent XSS attacks? . First of all, the components we coded are converted to ReactElement objects. DOM manipulation and generation are generated by Js scripts. Basically eliminated three XSS attacks.

React, however, provides dangerouslySetInnerHTML as an alternative to innerHTML. Let’s say there’s a scenario where the interface gives me JSON data. I need to show it in a div. If intercepted by an attacker, the JSON is replaced with a ReactElement structure. So what happens?

I wrote a demo here. What if I got rid of it? Typeof will find an error. And Symbol can not JSON, so the external is also unable to use dangerouslySetInnerHTML attack. Check the source code here

const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
    key: true.ref: true.__self: true.__source: true};function createElement(type, config, children) {
    let propName;

    // Reserved names are extracted
    const props = {};

    let key = null;
    let ref = null;

    if(config ! = =null) {
        if (hasValidRef(config)) {
            ref = config.ref;
        }
        if (hasValidKey(config)) {
            key = ' '+ config.key; }}// Filter the React reserved keywords
    for (propName in config) {
        if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
            props[propName] = config[propName];
        }
    }

    / / traverse the children
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
        props.children = children;
    } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
    }

    // Set the default props
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
            if (props[propName] === undefined) { props[propName] = defaultProps[propName]; }}}return ReactElement(type, key, ref, props);
}
Copy the code

The comments should be clear enough. Generate a ReactElement object based on the parameters, and bind the corresponding props, key, ref, etc.

Render process

Reactdom.render uses the reference here

In general, when writing applications with React, reactdom. render is the first function we fire. So let’s start with the reactdom. render entry function to analyze the render process.

Logical judgments and handling of Hydrate occur frequently in the source code. This is related to SSR combined with client rendering, will not do too much analysis. I will omit the source code

Reactdom. render is actually a reference to the Render method in ReactDOMLegacy, with condensed logic as follows:

export function render(
    // React. CreatElement's product
    element: React$Element<any>,
    container: Container,
    callback: ?Function.) {
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
    );
}
Copy the code

Actually call is legacyRenderSubtreeIntoContainer method, take a look at this

function legacyRenderSubtreeIntoContainer(parentComponent: ? React$Component<any, any>,// Usually null
    children: ReactNodeList,
    container: Container,
    forceHydrate: boolean,
    callback: ?Function.) {

    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;
    if(! root) {// [Q]: Initializes the container. Empty the nodes in the container and create FiberRoot
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate,
        );
        // FiberRoot; Starting point of application
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
            const originalCallback = callback;
            callback = function () {
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }
        // [Q]: Initialization cannot be batch processed, that is, synchronous update
        unbatchedUpdates(() = > {
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    } else {
        / / to omit... Similar to the above, except that there is no need to initialize the container and it can be batched
        // [Q] : What? What's the mystery of unbatchedUpdates
        updateContainer(children, fiberRoot, parentComponent, callback);
    }
    return getPublicRootInstance(fiberRoot);
}
Copy the code

In this step, the container is emptied of existing nodes, and any asynchronous callback is saved and FiberRoot references are bound for subsequent delivery of the correct root node, according to the FiberRoot documentation. I put two [Q] in the comments for two questions. Let’s take a closer look at these two problems

Initialize the

Look from the naming, legacyCreateRootFromDOMContainer is used to initialize the root node. LegacyCreateRootFromDOMContainer return the results to the container. The _reactRootContainer, while _reactRootContainer look from the code is as the basis of whether they have been initialized, and verify it. If you don’t believe me, open your React app and check the _reactRootContainer property of the container element

function legacyCreateRootFromDOMContainer(container: Container, forceHydrate: boolean,) :RootType {
  // omission hydrate...
  return createLegacyRoot(container, undefined);
}

export function createLegacyRoot(container: Container, options? : RootOptions,) :RootType {
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  / /!!!!!! look here
  this._internalRoot = createRootImpl(container, tag, options);
}
Copy the code

A series of function calls that return an instance of ReactDOMBlockingRoot. The important point is that the attribute _internalRoot is created through createRootImpl.

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // omission hydrate...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // omission hydrate...
  return root;
}

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :FiberRoot {
  / / generated FiberRoot
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // Generate Fiber object for Root
  const uninitializedFiber = createHostRootFiber(tag);
  // Bind FiberRoot to Fiber
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // Generate an update queue
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

export function initializeUpdateQueue<State> (fiber: Fiber) :void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    baseQueue: null.shared: {
      pending: null,},effects: null}; fiber.updateQueue = queue; }Copy the code

The general logic is to generate a FiberRoot object root. The Fiber object corresponding to root is generated, and the update queue of this Fiber is generated. It is clear when FiberRoot was initialized. Remember that FiberRoot is the starting point of the React application.

unbatchedUpdates

The English comments in the source code indicate that batch processing is not required and should be performed immediately. Its passed argument is a wrapper function that performs updateContainer. But you actually do updateContainer in the else judgment as well. So what’s the secret of unbatchedUpdates?

export function unbatchedUpdates<A.R> (fn: (a: A) => R, a: A) :R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    / /!!!!!! look here
    executionContext = prevExecutionContext;
    if(executionContext === NoContext) { flushSyncCallbackQueue(); }}}export function flushSyncCallbackQueue() {
  / / to omit...
  flushSyncCallbackQueueImpl();
}

// Clear the synchronization task queue
function flushSyncCallbackQueueImpl() {
  if(! isFlushingSyncQueue && syncQueue ! = =null) {
    isFlushingSyncQueue = true;
    let i = 0;
    try {
      const isSync = true;
      const queue = syncQueue;
      // Clear the queue with the highest priority
      runWithPriority(ImmediatePriority, () = > {
        for (; i < queue.length; i++) {
          let callback = queue[i];
          do {
            callback = callback(isSync);
          } while(callback ! = =null); }}); syncQueue =null;
    } catch (error) {
      // Remove the wrong task
      if(syncQueue ! = =null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // Resume execution at the next execution unit
      Scheduler_scheduleCallback(
        Scheduler_ImmediatePriority,
        flushSyncCallbackQueue,
      );
      throw error;
    } finally {
      isFlushingSyncQueue = false; }}}Copy the code

In the case of unbatchedUpdates, there is an extra logic in finally. The logic is mainly to refresh the synchronization task queue. Think about it. Why? So the synchronization task must be generated during the execution of FN (a). So let’s go ahead and check it out in updateContainer.

updateContainer

Note that the updateContainer is already in the Reconciler process. Follow up:

export function updateContainer(
    element: ReactNodeList, // The component to render
    container: OpaqueRoot, // OpaqueRoot is FiberRootparentComponent: ? React$Component<any, any>, callback: ?Function.) :ExpirationTimeOpaque {
    // Root node Fiber
    const current = container.current;
    const eventTime = requestEventTime();

    const suspenseConfig = requestCurrentSuspenseConfig();
    // [Q]: Calculate the expiration time of this task
    const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );

    const context = getContextForSubtree(parentComponent);
    if (container.context === null) {
        container.context = context;
    } else {
        container.pendingContext = context;
    }

    // Create an update task
    const update = createUpdate(eventTime, expirationTime, suspenseConfig);
    update.payload = { element };

    callback = callback === undefined ? null : callback;
    if(callback ! = =null) {
        update.callback = callback;
    }

    // Insert the task into Fiber's update queue
    enqueueUpdate(current, update);
    // scheduleWork is scheduleUpdateOnFiber
    scheduleWork(current, expirationTime);

    return expirationTime;
}
Copy the code

EnqueueUpdate = expirationTime = expirationTime = expirationTime = expirationTime Finally, scheduleUpdateOnFiber is used to schedule the task.

ExpirationTime calculation

ExpirationTime is a very important concept. React prevents an update from being interrupted due to priority. React sets a expirationTime. When a expirationTime is expirationTime, if an update has not been executed, React will force that update. That’s what expirationTime does.

This is the first time we have encountered its computational logic. Let’s break it down.

The first step is to calculate currentTime, which is essentially a context time with the current timestamp converted to the built-in ExpirationTime. Take a look at

// 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;
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;

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

export function msToExpirationTime(ms: number) :ExpirationTime {
    return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

export function requestCurrentTimeForUpdate() {
    / / to omit...
    return msToExpirationTime(now());
}
Copy the code

Take a look at computeExpirationForFiber concrete calculation logic

export function computeExpirationForFiber(
    currentTime: ExpirationTime,
    fiber: Fiber,
    suspenseConfig: null | SuspenseConfig,
) :ExpirationTime {
    const mode = fiber.mode;
    // Synchronous mode
    if ((mode & BlockingMode) === NoMode) {
        return Sync;
    }
    // Get the current priority from Scheduler
    const priorityLevel = getCurrentPriorityLevel();
    if ((mode & ConcurrentMode) === NoMode) {
        return priorityLevel === ImmediatePriority ? Sync : Batched;
    }

    // ...

    let expirationTime;
    switch (priorityLevel) {
        case ImmediatePriority:
            expirationTime = Sync;
            break;
        case UserBlockingPriority:
            // Same as computeAsyncExpiration. The difference is that the expirationInMs parameter value is smaller.
            // Therefore, the smaller the expirationTime, the higher the priority
            expirationTime = computeInteractiveExpiration(currentTime);
            break;
        case NormalPriority:
        case LowPriority: // TODO: Handle LowPriority
            // TODO: Rename this to... something better.
            expirationTime = computeAsyncExpiration(currentTime);
            break;
        case IdlePriority:
            expirationTime = Idle;
            break;
        default:
            invariant(false.'Expected a valid priority level'); }}export const LOW_PRIORITY_EXPIRATION = 5000;
// This BATCH means that?
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
    currentTime: ExpirationTime,
) :ExpirationTime {
    return computeExpirationBucket(
        currentTime,
        LOW_PRIORITY_EXPIRATION,
        LOW_PRIORITY_BATCH_SIZE,
    );
}

function ceiling(num: number, precision: number) :number {
    return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
    currentTime,
    expirationInMs, / / 5000
    bucketSizeMs, / / 250
) :ExpirationTime {
    return (
        MAGIC_NUMBER_OFFSET -
        ceiling(
            MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
            bucketSizeMs / UNIT_SIZE,
        )
    );
}
Copy the code

In summary, the calculation formula is as follows:

// current = MAGIC_NUMBER_OFFSET - ((now() / UNIT_SIZE) | 0);
// expirationTime = MAGIC_NUMBER_OFFSET - ((((MAGIC_NUMBER_OFFSET - currentTime + 500) / 25) | 0) + 1) * 25
// => MAGIC_NUMBER_OFFSET - ((((((now() / UNIT_SIZE) | 0) + 500) / 25) | 0) + 1) * 25
Copy the code

The | 0 is used for integer. Notice the + 1 operation, what does that say? The difference between two different expirationTime is a multiple of 25, that is, within 25ms tasks are the same expirationTime. So updates for 25ms will be combined into a single task.

As described on the website, Legacy mode has automatic batching in compositing events, but is limited to one browser task. To use this function, non-React events must use unstable_batchedUpdates. In blocking and concurrent modes, all setStates are batch by default. Here are two examples to help you understand:

  • Non-concurrent mode setState
  • Model of concurrent setState

After analyzing the calculation of expirationTime, continue to look at the logic of scheduleUpdateOnFiber.

From here on, there are synchronous and asynchronous processing methods in the source code, synchronous tasks are not scheduled by Scheduer. For the integrity of the analysis, we will examine only asynchronous processes. ExpirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime = expirationTime But it means different things at different stages. To be sure, it determines whether the component is updated or not, or when it is updated.

export function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTimeOpaque,) {
  / / get FiberRoot
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    return null;
  }
  if (expirationTime === Sync) {
    // Synchronize task scheduling
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
  / / to omit...
}
Copy the code

ScheduleUpdateOnFiber is simply used to update the expiration time of the entire “tree” Root of the current node. One priority is the ensureRootIsScheduled method

// This function is used to schedule tasks. A root(Fiber node) can only have one task running
// If a task is already scheduled, the expiration time of the existing task is the same as that of the next task.
// This function is called every update and before the task exits
// Note: root is FiberRoot
function ensureRootIsScheduled(root: FiberRoot) {
    // lastExpiredTime indicates the expiration time
    const lastExpiredTime = root.lastExpiredTime;
    if(lastExpiredTime ! == NoWork) {// Special case: Expired work should be refreshed synchronously
        root.callbackExpirationTime = Sync;
        root.callbackPriority = ImmediatePriority;
        root.callbackNode = scheduleSyncCallback(
            performSyncWorkOnRoot.bind(null, root),
        );
        return;
    }
    // The next nearest due date
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    // Root has scheduling tasks in progress
    const existingCallbackNode = root.callbackNode;
    if (expirationTime === NoWork) {
        if(existingCallbackNode ! = =null) {
            root.callbackNode = null;
            root.callbackExpirationTime = NoWork;
            root.callbackPriority = NoPriority;
        }
        return;
    }

    // Get the expiration time of the current task; All updates of the same priority that occur in the same event receive the same expiration time
    const currentTime = requestCurrentTimeForUpdate();
    // Calculate the priority of the current task based on the expiration time of the next scheduled task and the expiration time of the current task
    // If currentTime is smaller than expirationTime, it has a higher priority
    const priorityLevel = inferPriorityFromExpirationTime(
        currentTime,
        expirationTime,
    );

    // If the priority of the task being processed is based on this task, cancel the task being processed!
    if(existingCallbackNode ! = =null) {
        const existingCallbackPriority = root.callbackPriority;
        const existingCallbackExpirationTime = root.callbackExpirationTime;
        if (
            // Tasks must have exactly the same due time.
            existingCallbackExpirationTime === expirationTime &&
            // Compare the priorities of the two tasks
            existingCallbackPriority >= priorityLevel
        ) {
            return;
        }
        // Cancel the task
        cancelCallback(existingCallbackNode);
    }

    // Update expiration time and priority
    root.callbackExpirationTime = expirationTime;
    root.callbackPriority = priorityLevel;

    let callbackNode;
    if (expirationTime === Sync) {
        / / to omit...
        / / tasks will be pushed into the synchronization task queue here, in front of the analysis to the flushSyncCallbackQueueImpl empty task is to push from here
    } else {
        // Push tasks to the Scheduler scheduling queue
        callbackNode = scheduleCallback(
            priorityLevel,
            / / binding
            performConcurrentWorkOnRoot.bind(null, root),
            // Calculate the timeout
            { timeout: expirationTimeToMs(expirationTime) - now() },
        );
    }

    // Update Fiber's current callback point
    root.callbackNode = callbackNode;
}
Copy the code

The primary logic in ensureRootIsScheduled has three steps:

  1. Calculate the expiration time and priority of this task.
  2. If a task is being scheduled on the node. If the expiration time is the same and the priority of existing tasks is higher, the scheduling task is cancelled. Otherwise, the existing task is cancelled.
  3. Push the task inSchedulerAnd set its priority and task expiration time

Each section of this code can be extended for analysis. But I’m focusing on the general flow here, so I’m focusing on schedulecallback-related logic. For other parts, I will have time for further analysis later.

The scheduleCallback assigns a task’s execution function to the Scheduler. The subsequent process needs to wait for the Scheduler to trigger specific performConcurrentWorkOnRoot executive function. The render process on the first temporary analysis so far.

Summary of Render process

  1. renderWill be calledlegacyRenderSubtreeIntoContainermethods
  2. legacyRenderSubtreeIntoContainerIf it is the first rendering, it will be initialized firstFiberRoot, which is the starting point of application. Generate the root node at the same timeFiberInstance. hereFiberRoot.current = Fiber; Fiber.stateNode = FiberRoot.
  3. callupdateContainerThe expiration time of this update is calculated. And generate task objectsupdate, insert itFiberUpdate queue, and then callscheduleUpdateOnFiberTriggering task scheduling
  4. scheduleUpdateOnFiberThe expiration time of the entire Fiber tree with the Fiber node as the root node is updated. And then callensureRootIsScheduledschedule
  5. ensureRootIsScheduledTo bind tasks to specific execution functions. And then passed on toSchedulerTo deal with

SetState process

Before moving on to the subsequent Reconciler and Renderer details, let’s familiarize ourselves with the setState process while the iron is hot. Since this. SetState is invoked, look for it in Component. Look at the act baseclasses

const emptyObject = {};
function Component(props, context, updater) {
    this.props = props;
    this.context = context;
    this.refs = emptyObject;
    // ReactNoopUpdateQueue is a meaningless empty object
    this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.setState = function (partialState, callback) {
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code

The initial structure of Component is simple. We see the setState method is invoked in this. Updater. EnqueueSetState method, but the update is the default empty useless objects, we haven’t passed in the constructor usually an update parameter, it shows this method is certainly subsequent injection. And I looked and looked and found a similar thing classComponentUpdater

const classComponentUpdater = {
    isMounted,
    enqueueSetState(inst, payload, callback) {
        const fiber = getInstance(inst);
        const currentTime = requestCurrentTimeForUpdate();
        const suspenseConfig = requestCurrentSuspenseConfig();
        const expirationTime = computeExpirationForFiber(
            currentTime,
            fiber,
            suspenseConfig,
        );
        // Generate the update object for this setState
        const update = createUpdate(expirationTime, suspenseConfig);
        update.payload = payload;
        if(callback ! = =undefined&& callback ! = =null) {
            update.callback = callback;
        }
        // Update quests to join the team
        enqueueUpdate(fiber, update);
        scheduleWork(fiber, expirationTime);
    },
    enqueueReplaceState(inst, payload, callback) {
        // Same as above
    },
    enqueueForceUpdate(inst, callback) {
        // Same as above}};Copy the code

The logic in enqueueSetState is a bit familiar. The render process is the same as the updateContainer process. Check back if you don’t remember. The update property of a classComponentUpdater is injected into the Component.

In the previous analysis of the Render process, we only analyzed the generation of task fragments and push into the scheduling queue, and did not analyze the initialization of components. Does the React constructor inject the Component when initializing it? Follow this train of thought for the next step of analysis. First of all, let’s look at a piece of code in the beginWork method, and the beginWork method will be analyzed in detail later. This is the Fiber object used to create the child component.

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
) :Fiber | null {
    // Try to reuse the current node
    if(current ! = =null) {
        / / to omit...
    }
    // If you cannot reuse it, update or mount it
    switch (workInProgress.tag) {
        / / to omit...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        / / to omit...}}Copy the code

The code in beginWork is divided into two parts. Used to handle mount and update logic, respectively. The process we analyze is the first initialization, so the mount process is followed. BeginWork calls different methods based on the tag, so let’s look at the updateClassComponent first

function updateClassComponent(
    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    nextProps,
    renderExpirationTime: ExpirationTime,
) {
    // omit context handling...

    // An instance of the component
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    // Instance null indicates that the component was rendered for the first time
    if (instance === null) {
        if(current ! = =null) {
            // Reset current and WIP dependencies (backup)
            current.alternate = null;
            workInProgress.alternate = null;
            // mark it as a new node
            workInProgress.effectTag |= Placement;
        }
        // Initialize the component instance
        constructClassInstance(workInProgress, Component, nextProps);
        / / mount; And invoke the appropriate lifecycle
        mountClassInstance(
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
        shouldUpdate = true;
    } else {
        // omit update logic...
    }
    // TODO: execute render to create subfiber.
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}
Copy the code
function constructClassInstance(workInProgress: Fiber, ctor: any, props: any,) :any {
    let context = emptyContextObject;
    // omit context related logic...

    const instance = new ctor(props, context);
    conststate = (workInProgress.memoizedState = instance.state ! = =null&& instance.state ! = =undefined
            ? instance.state
            : null);
    adoptClassInstance(workInProgress, instance);

    // omit context related logic...
    return instance;
}
Copy the code
function adoptClassInstance(workInProgress: Fiber, instance: any) :void {
    instance.updater = classComponentUpdater;
    workInProgress.stateNode = instance;
    // Bind the instance to Fiber for subsequent updates
    setInstance(instance, workInProgress);
}
Copy the code

You can see that when instance is null, the following processes are executed

  1. And mark the currenteffectTagforPlacementIs a new node
  2. Initialize to an instance and bind toFiber(workInProgress)And bindupdateattribute
  3. Finally, mountClassInstance is called to mount the node and invoke the associated lifecycle.

At this point, the subsequent update process is consistent with the render process, do not repeat analysis ~

Scheduler

Scheduler is a rIdc polyfill implemented separately by the React team for task scheduling. The React team’s intention is not limited to just one application scenario, but also to serve more businesses and become a tool for wider application.

Minimum priority queue

Since tasks have different expiration times and priorities, a data structure is needed to manage priority tasks. React data structures with a smaller expirationTime would have a higher priority. React is a minimal priority queue based on the small top heap. Let’s just look at the code. SchedulerMinHeap

type Heap = Array<Node>;
type Node = {|
  id: number,
    sortIndex: number,
|};

// Insert to the end of the heap
export function push(heap: Heap, node: Node) :void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

// Get the heap top task with the smallest sortIndex/id
export function peek(heap: Heap) :Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

// Delete the heap top task
export function pop(heap: Heap) :Node | null {
  const first = heap[0];
  if(first ! = =undefined) {
    const last = heap.pop();
    if(last ! == first) { heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null; }}// Keep the small top heap up
function siftUp(heap, node, i) {
  let index = i;
  while (true) {
    // bit operation; The parent node is -> I / 2-1
    const parentIndex = (index - 1) > > >1;
    const parent = heap[parentIndex];
    if(parent ! = =undefined && compare(parent, node) > 0) {
      // parent larger, switch places
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return; }}}// Keep the small top heap down
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // // If the left or right child node is smaller than the target node (parent node), the switch is performed
    if(left ! = =undefined && compare(left, node) < 0) {
      if(right ! = =undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else{ heap[index] = left; heap[leftIndex] = node; index = leftIndex; }}else if(right ! = =undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return; }}}function compare(a, b) {
  // Compare sort index first, then task id.
  // Compare task ID with sort index
  const diff = a.sortIndex - b.sortIndex;
  returndiff ! = =0 ? diff : a.id - b.id;
}

Copy the code

The implementation is to use an array to simulate the structure of a minimal heap. As can be seen, each task insertion or removal will restore the minimum heap structure, sorting rules are supplemented by sortIndex, taskId. In React, sortIndex corresponds to the expiration time, while taskId corresponds to the increasing task sequence. This will be discussed later.

Enabling Task Scheduling

A task node is generated under ensureRootIsScheduled and the task is pushed into the Scheduler via scheduleCallback. So we first from this task into the team method to step by step analysis

var taskIdCounter = 1;

// Currently, Scheduler's API is unstate_, indicating that it is not a stable version
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // It is more accurate to call performance.now() or date.now ()
  var currentTime = getCurrentTime();

  var startTime;
  var timeout;
  // Determine the start time based on whether there is a delay
  if (typeof options === 'object'&& options ! = =null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    // [Q1]: this parameter is directly used if the timeout configuration is enabled. Otherwise, the value is calculated based on the priority
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel);
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }

  // The expiration time is equal to the start time + the timeout time
  var expirationTime = startTime + timeout;

  // This is the data structure of a task.
  var newTask = {
    // Tasks with the same timeout are compared with ids, so it is first come, first served
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1};if (enableProfiling) {
    newTask.isQueued = false;
  }

  // [Q2] : There is a timerQueue and a taskQueue.
  if (startTime > currentTime) {
    // This is a delayed task.
    // This is a deferred task; Options. delay exists
    newTask.sortIndex = startTime;
    // If the start time is longer than the current time, push it into the timer queue, indicating that this is a wait queue
    push(timerQueue, newTask);
    // If the task queue is empty, all tasks are delayed, and newTask is the earliest delayed task.
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // If the timeout process is in progress, cancel first and restart later
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Initiate a timeout processingrequestHostTimeout(handleTimeout, startTime - currentTime); }}else {
    newTask.sortIndex = expirationTime;
    // Non-deferred tasks are thrown to the task queue
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // If not, start scheduling;
    if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
      // [Q] Enable schedulingrequestHostCallback(flushWork); }}// [A] : Return the reference to this task
  return newTask;
}
Copy the code

In this code, you can see the data structure of a scheduled task, and the tasks are sorted according to sortIndex, which is the expirationTime of a task, and id is an increasing sequence. Several issues are noted in the notes, which will be analyzed in detail below

The timeout to calculate

// Execute immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// User behavior is blocked
var USER_BLOCKING_PRIORITY = 250;
// The default expiration time is five seconds
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// It will never expire. MaxSigned31BitInt is v8. 32 is the maximum valid value of the system
var IDLE_PRIORITY = maxSigned31BitInt;

function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY;
    case IdlePriority:
      return IDLE_PRIORITY;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      returnNORMAL_PRIORITY_TIMEOUT; }}Copy the code

As you can see, the priority is converted to the constant time. The higher the priority, the lower the timeout time.

taskQueue & timerQueue

In the conditional branch startTime > currentTime, the task is pushed to taskQueue and timerQueue, respectively. These two queues are actually the smallest heap structure we analyzed earlier. TaskQueue represents the currently scheduled task, while timerQueue represents the delayed taskQueue. During the task scheduling process, tasks in the timerQueue are continuously transferred to the taskQueue, which will be discussed later.

The specific process of scheduling

We can see that when a task is inserted into the schedule queue, if it is not scheduled at that time, the requestHostCallback method is called to start scheduling and a flushwork is passed in as the input function.

requestHostCallback = function(callback) {
  // The incoming callback is cached
  scheduledHostCallback = callback;
  // Whether it is in the message loop
  if(! isMessageLoopRunning) { isMessageLoopRunning =true;
    port.postMessage(null); }};Copy the code

RHC only caches the callback (flushwork) function. And sent an empty message. So the focus is on what this port is. This is where React emulates the requestIdleCallback.

MessageChannel simulates rIC to achieve cyclic scheduling

For those unfamiliar with MessageChannel, check it out. Let’s take a look at how Scheduler is used.

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
Copy the code

As you can see, when a message is generated using port.postMessage, the function that actually processes the message is performWorkUntilDeadline.

let isMessageLoopRunning = false;
let scheduledHostCallback = null;

const performWorkUntilDeadline = () = > {
  // scheduledHostCallback is assigned by scheduledHostCallback
  if(scheduledHostCallback ! = =null) {
    const currentTime = getCurrentTime();
    // [Q]: End time = Current time + yieldInterval
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      // Are there any remaining tasks? ScheduledHostCallback may be flushwork
      const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
      );
      if(! hasMoreWork) {// No more tasks stop the loop and clear the scheduledHostCallback reference
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // If there are still tasks, continue to send messages. It's like a recursive operation
        port.postMessage(null); }}catch (error) {
      // If a task goes wrong. Skip the next task and throw an error
      port.postMessage(null);
      throwerror; }}else {
    // Reset the loop state
    isMessageLoopRunning = false;
  }
  // [Q]: I don't know what this is
  needsPaint = false;
};
Copy the code

As usual, there are a few things that need to be analyzed carefully.

yieldInterval

From the name and usage, I think it should stand for the execution time of the task.

// The default is 5
let yieldInterval = 5;

forceFrameRate = function (fps) {
  / /??? Look down on me 144Hz
  if (fps < 0 || fps > 125) {
    console['error'] ('forceFrameRate takes a positive int between 0 and 125, ' +
      'forcing framerates higher than 125 fps is not unsupported',);return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5; }};Copy the code

ForceFrameRate is an EXTERNAL API interface used to dynamically configure the execution period of scheduled tasks.

deadline & needsPaint

let deadline = 0;
let maxYieldInterval = 300;
let needsPaint = false;

if( enableIsInputPending && navigator ! = =undefined&& navigator.scheduling ! = =undefined&& navigator.scheduling.isInputPending ! = =undefined
) {
  const scheduling = navigator.scheduling;
  shouldYieldToHost = function () {
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      // There is no time. You may want to cede control to the main thread so that the browser can perform high-priority tasks, primarily drawing and user input
      // Therefore, if there is drawing or user input, it should be abandoned and put back true
      // If neither exists, it is possible to reduce production while maintaining responsiveness
      // But there are draw status updates or other mainline tasks that are not 'requestPaint' initiated (such as network events)
      // At some point control must be ceded
      if (needsPaint || scheduling.isInputPending()) {
        // Pending drawing or user input
        return true;
      }
      // There is no drawing or input to be processed. But control also needs to be released when maximum production intervals are reached
      return currentTime >= maxYieldInterval;
    } else {
      return false; }}; requestPaint =function () {
    needsPaint = true;
  };
} else {
  shouldYieldToHost = function () {
    return getCurrentTime() >= deadline;
  };

  requestPaint = function () {}; }Copy the code

The first thing to make clear is that shouldYieldToHost and requestPaint are interface functions that Scheduler provides. Specific use will be analyzed in place later.

Deadline is used to check if the schedule timed out in shouldYieldToHost. By default, compare currentTime and Deadline. However, in an environment that supports navigator.scheduling, React will have more considerations, that is, browser drawing and user input should be limited response, otherwise the scheduling time can be appropriately extended.

Here is a summary of the scheduling start process, so that the brain is not confused.

  1. requestHostCallbackPrepare tasks to be performedscheduledHostCallback
  2. requestHostCallbackExample Start the task scheduling cycle
  3. MessageChannelReceive the message and invokeperformWorkUntilDeadlinePerform a task
  4. performWorkUntilDeadlineIs calculated firstdeadline. And then execute the mission
  5. After a task is executed, the return value is used to determine whether there is a next task. If so, recursive execution is achieved through a message loopperformWorkUntilDeadline. Otherwise, the message loop ends

The logic of task scheduling cycle execution was only analyzed. The specific task is to flushWork, the reference function of scheduledHostCallback.

Task execution

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if(currentTask ! = =null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throwerror; }}else {
      // No catch in prod codepath.
      // The official comment says that the build environment does not catch errors thrown by workLoop
      returnworkLoop(hasTimeRemaining, initialTime); }}finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      constcurrentTime = getCurrentTime(); markSchedulerSuspended(currentTime); }}}Copy the code

FlushWork’s work is simple. It just resets some flags and returns the result of the workLoop execution. So the focus must be on this function.

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // [Q]: What is this?
  advanceTimers(currentTime);
  // Take out the top task. The highest priority task
  currentTask = peek(taskQueue);
  while( currentTask ! = =null &&
    // debug is used! (enableSchedulerDebugging && isSchedulerPaused) ) {if (
      // If the task has not expired and the deadline of the current scheduling period has arrived, the task will be postponed to the next scheduling period. shouldYieldToHost
      currentTask.expirationTime > currentTime &&
      // These two are analyzed previously; HasTimeRemaining is always true, so what's the point??(! hasTimeRemaining || shouldYieldToHost()) ) {break;
    }
    const callback = currentTask.callback;
    if(callback ! = =null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // Calculate whether the current task has timed out
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      / / [Q] : implement the callback, such as performConcurrentWorkOnRoot render process analysis to the front
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // continuationCallback replaces the callback of the current task
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // continuationCallback is not valid and is ejected from the task queue
        // To prevent the task from being picked up elsewhere, judge
        if(currentTask === peek(taskQueue)) { pop(taskQueue); }}// em.... Is it
      advanceTimers(currentTime);
    } else {
      // The task was cancelled, and the task was ejected
      // review the case of cancelCallback called "ensureRootIsScheduled"
      pop(taskQueue);
    }
    // Take the task from the top again
    // Note: If continuationCallback is valid, there is no pop current task. This time, it's still the current task
    currentTask = peek(taskQueue);
  }
  // performWorkUntilDeadline = hasMoreWork
  if(currentTask ! = =null) {
    return true;
  } else {
    // [Q] : Checks whether the task in the delay queue is expired
    let firstTimer = peek(timerQueue);
    if(firstTimer ! = =null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false; }}Copy the code

The general process notes are detailed. As usual, analyze a few problems with the notes.

advanceTimers

function advanceTimers(currentTime) {
  // Iterate over tasks in timerQueue; Move timed tasks to the taskQueue
  let timer = peek(timerQueue);
  while(timer ! = =null) {
    if (timer.callback === null) {
      // The task was cancelled
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timeout task transfer
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true; }}else {
      // Continue to hang undated
      return; } timer = peek(timerQueue); }}Copy the code

The wookLoop function entry calls advanceTimers for the first time to reorganize tasks and refresh the task queue. After that, each call in the while takes a certain amount of time to execute, so the task queue needs to be refreshed again after execution.

continuationCallback

First, continuationCallback is determined by callback. The return value of callback may be a function that indicates that the current task should be reprocessed. I’ll leave you with a question that we’ll explore further when we look at the actual implementation of callback

requestHostTimeout & handleTimeout

At the end of the wookLoop, currentTask === NULL checks whether the task in the delay queue has expired.

requestHostTimeout = function (callback, ms) {
  taskTimeoutID = setTimeout(() = > {
    callback(getCurrentTime());
  }, ms);
};

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // Rearrange the task queue
  advanceTimers(currentTime);

  // isHostCallbackScheduled is true. It means there's a new mission coming in
  if(! isHostCallbackScheduled) {Execute if advanceTimers above comb out expired delayed tasks to the task queue
    if(peek(taskQueue) ! = =null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // Otherwise call the method recursively
      const firstTimer = peek(timerQueue);
      if(firstTimer ! = =null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); }}}}Copy the code

As you can see, this is actually after the task in the task queue has completed. Recursively queries whether there are expired tasks in the delay queue. If there are expired tasks, they are transferred to the task queue and executed.

At this point, the complete process of Scheduler from task listing, to circular scheduling, to task execution has been analyzed. Make a simple process summary:

  1. unstable_scheduleCallbackCreates a task and pushes it to the delay queue if the task is delayedtimerQueueOtherwise, the task is pushed to the queuetaskQueue
  2. Is called if the task created is a deferred taskrequestHostTimeoutMethods usingsetTimeouttoRecursively detects whether a task is expired. Otherwise, the task is scheduledrequestHostCallback
  3. requestHostCallbackthroughMessageChanneltheport2Send a message toport1, the specific processing function isperformWorkUntilDeadline
  4. performWorkUntilDeadlineThe deadline of this dispatch will be calculated and used at the same timeMessage loopTo perform tasks recursively
  5. Task specific handling bywookLoopThe execution. It removes tasks from the task queuetaskQueueThe top of the heap is removed and executed in turn. Is called if the task queue is emptyrequestHostTimeoutEnable recursive detection.

Reconciler

After analyzing the Scheduler’s logic, the Reconciler’s logic is then analyzed. Much of our well-worn logic for Diff updates takes place in the Reconciler phase, where a lot of component updating is calculated and optimized.

The scheduling process of Scheduler is analyzed above. And specific implement the callback is performConcurrentWorkOnRoot in the Scheduler. Let’s take a look

// The entry function called by the Scheduler
function performConcurrentWorkOnRoot(root, didTimeout) {
    / / reset
    currentEventTime = NoWork;

    if (didTimeout) {
        // The task timed out
        const currentTime = requestCurrentTimeForUpdate();
        // Mark the expiration time as current to process expired work synchronously in a single batch.
        markRootExpiredAtTime(root, currentTime);
        // Schedule a synchronization task
        ensureRootIsScheduled(root);
        return null;
    }

    // Get the next expiration (update) time. This will be judged as necessary to perform this rendering
    const expirationTime = getNextRootExpirationTimeToWorkOn(root);
    if(expirationTime ! == NoWork) {const originalCallbackNode = root.callbackNode;

        // TODO:Refresh passive Hooks
        flushPassiveEffects();

        // If the root or expiration time has changed, the existing stack is discarded and a new stack is prepared. Otherwise, we'll pick up where we left off.
        if( root ! == workInProgressRoot || expirationTime ! == renderExpirationTime ) {// [Q]: reset data;
            // Set renderExpirationTime to expirationTime
            // Copy root.current to workInProgress, etc
            prepareFreshStack(root, expirationTime);
            startWorkOnPendingInteractions(root, expirationTime);
        }

        if(workInProgress ! = =null) {
            / / to omit...
            do {
                try {
                    workLoopConcurrent();
                    break;
                } catch(thrownValue) { handleError(root, thrownValue); }}while (true);
            / / to omit...
        }

        if(workInProgress ! = =null) {
            // There is still work to be done. Exit without submission.
            stopInterruptedWorkLoopTimer();
        } else {
            stopFinishedWorkLoopTimer();

            const finishedWork: Fiber = ((root.finishedWork =
                root.current.alternate): any);
            root.finishedExpirationTime = expirationTime;
            / / commit; Start the Renderer processfinishConcurrentRender( root, finishedWork, workInProgressRootExitStatus, expirationTime, ); }}return null;
}
Copy the code

The system checks whether the task times out. If the task times out, the system synchronizes the task to prevent task interruption. If there is no timeout, do some initialization in prepareFreshStack first. The workLoopConcurrent loop is then entered.

prepareFreshStack

// The expiration time of this render
let renderExpirationTime: ExpirationTime = NoWork;

function prepareFreshStack(root, expirationTime) {
    / / to omit...
    if(workInProgress ! = =null) {
        // workInProgress not empty indicates that there was an interrupted task before. To give up
        let interruptedWork = workInProgress.return;
        while(interruptedWork ! = =null) {
            unwindInterruptedWork(interruptedWork);
            interruptedWork = interruptedWork.return;
        }
    }
    workInProgressRoot = root;
    // Copy wIP from current; And reset effectList
    workInProgress = createWorkInProgress(root.current, null);
    // Set renderExpirationTime to the next expiration time
    renderExpirationTime = expirationTime;
    / / to omit...
}
Copy the code

If the current WIP is not empty, it indicates that the last interrupted task was performed. Cancel the interrupted task by backtracking all the way up to the root node. Then get the expiration date of the next task from FiberRoot at the same time and assign renderExpirationTime as the expiration date of this render.

workLoopConcurrent

The code for workLoopConcurrent was posted at the beginning of this article, so take a look again

function workLoopConcurrent() {
  while(workInProgress ! = =null && !shouldYield()) {
    // workInProgress is FiberRoot Fiber
    // The last return value (subfiber) is then used as an input parameterworkInProgress = performUnitOfWork(workInProgress); }}Copy the code

The work of workLoopConcurrent is mainly to compare current and workInProgress Fiber trees. Put an effectTag on the changed Fiber in the WIP. DOM nodes are also updated/created from the bottom up to form an off-screen DOM tree, which is processed by the Renderer.

Recursion based on loop

Post a truncated version of the code flow before familiarizing yourself with it. Here is not according to the routine card, first according to personal understanding to make a summary. This with a general idea of the structure may be better to understand the subsequent source code.

function performUnitOfWork(unitOfWork: Fiber) :Fiber | null {
    // Old Fiber for comparison
    const current = unitOfWork.alternate;

    / / to omit...
    // [Q]: Process the current Fiber node and return the next child node Fiber
    let next = beginWork(current, unitOfWork, renderExpirationTime);

    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    // There are no child nodes
    if (next === null) {
        next = completeUnitOfWork(unitOfWork);
    }

    ReactCurrentOwner.current = null;
    return next;
}

// Try to complete the current Fiber, then move to the next level. If there are no more siblings, return parent fiber.
function completeUnitOfWork(unitOfWork: Fiber) :Fiber | null {
    workInProgress = unitOfWork;
    do {
        // Old Fiber for comparison
        const current = workInProgress.alternate;
        const returnFiber = workInProgress.return;

        // Check if the work completed or if something threw.
        if ((workInProgress.effectTag & Incomplete) === NoEffect) {
            // [Q]: Create/update the node instance corresponding to the current Fiber
            let next = completeWork(current, workInProgress, renderExpirationTime);
            stopWorkTimer(workInProgress);
            resetChildExpirationTime(workInProgress);

            if(next ! = =null) {
                // New child nodes are generated
                return next;
            }

            // [Q]: A list of effectLists is created
            // Start with...
        } else {
            // An exception was thrown. The decision to catch or throw an exception depends on whether it is boundary
            / / to omit...
        }

        const siblingFiber = workInProgress.sibling;
        // Whether sibling nodes exist
        if(siblingFiber ! = =null) {
            return siblingFiber;
        }
        workInProgress = returnFiber;
    } while(workInProgress ! = =null);

    if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted;
    }
    return null;
}
Copy the code

Begin the beginWork node operation and create a child node. The child node is returned as Next or if there is a next. After returning to workLoopConcurrent, workLoopConcurrent determines whether it is expired or not, and if it is not, it calls the method again.

If next does not exist, it means that the current node has traversed down to the bottom of the child node, meaning that the branch of the subtree has been traversed and is ready to do its job. Perform completeUnitOfWork in the following steps

  1. completeUnitOfWorkFirst callcompleteWorkCreate/update the currentFiberCorresponding node instances (such as native DOM nodes)instance, while the child has been updatedFiberIs inserted into theinstanceForm an off-screen render tree.
  2. There are currentlyFiberThe node iseffectTagAppends it toeffectListIn the
  3. Find if there aresiblingSibling node, if yes, return the sibling node, because this node may also have child nodes, need to passbeginWorkPerform operations.
  4. If there are no sibling nodes. All the way up and back untilrootNode or found on a nodesiblingSibling nodes.
  5. If theroot, so is its returnnull, which means the whole tree traversal is done, okcommit. If a sibling is encountered in the middle, it is the same as the first3step

The text may not be very clear, but look directly at an example:

The execution sequence is:

The text “hello” node does not execute beginWork/completeWork because React handles Fiber with a single text child node

1. App beginWork
2. div Fiber beginWork
3. span Fiber beginWork
4. span Fiber completeWork
5. div Fiber completeWork
6. p Fiber beginWork
7. p Fiber completeWork
8. App Fiber completeWork
Copy the code

beginWork

BeginWork has analyzed the logic corresponding to the mount stage in the previous analysis of setState. So I’ll just analyze the update logic here. Let’s take a look at the general work of beginWork.

/ * * *@param {*} Current Old Fiber *@param {*} WorkInProgress new Fiber *@param {*} RenderExpirationTime Expiration time *@returns Sub-component Fiber */
function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
) :Fiber | null {
    const updateExpirationTime = workInProgress.expirationTime;

    // Try to reuse the current node
    if(current ! = =null) {
        / / to omit...
        / / reuse current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    workInProgress.expirationTime = NoWork;

    // If you cannot reuse it, update or mount it
    switch (workInProgress.tag) {
        / / to omit...
        case ClassComponent: {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps =
                workInProgress.elementType === Component
                    ? unresolvedProps
                    : resolveDefaultProps(Component, unresolvedProps);
            return updateClassComponent(
                current,
                workInProgress,
                Component,
                resolvedProps,
                renderExpirationTime,
            );
        }
        case HostRoot:
            return updateHostRoot(current, workInProgress, renderExpirationTime);
        case HostComponent:
            return updateHostComponent(current, workInProgress, renderExpirationTime);
        case HostText:
            return updateHostText(current, workInProgress);
        / / to omit...}}Copy the code

We continue with the updateClassComponent we analyzed earlier to analyze the update flow.

function updateClassComponent(

    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    nextProps,
    renderExpirationTime: ExpirationTime,
) {

    // Process the context logic ahead of time. Omit...

    // An instance of the component
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    if (instance === null) {
        // mount. wip.effectTag = Placement
        / / to omit...
    } else {
        // update. wip.effectTag = Update | Snapshot
        / / before invoking render the life cycle of getDerivedStateFromProps | UNSAFE_componentWillReceiveProps (maybe two)
        // Then call shouldComponentUpdate to see if an update is needed
        // Finally update props and state
        shouldUpdate = updateClassInstance(
            current,
            workInProgress,
            Component,
            nextProps,
            renderExpirationTime,
        );
    }
    // execute render to create a sub-fiber.
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
    return nextUnitOfWork;
}

function finishClassComponent(
    current: Fiber | null,
    workInProgress: Fiber,
    Component: any,
    shouldUpdate: boolean,
    hasContext: boolean,
    renderExpirationTime: ExpirationTime,
) {
    // The reference should be updated, even if shouldComponentUpdate returns false
    markRef(current, workInProgress);

    constdidCaptureError = (workInProgress.effectTag & DidCapture) ! == NoEffect;// Reuse current without updating and without sending errors
    if(! shouldUpdate && ! didCaptureError) {if (hasContext) {
            invalidateContextProvider(workInProgress, Component, false);
        }
        / / reuse current
        return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderExpirationTime,
        );
    }

    const instance = workInProgress.stateNode;

    // Rerender
    ReactCurrentOwner.current = workInProgress;
    let nextChildren = instance.render();

    // PerformedWork is provided for React DevTools to read
    workInProgress.effectTag |= PerformedWork;
    if(current ! = =null && didCaptureError) {
        // Error.
        / / to omit...
    } else {
        reconcileChildren(
            current,
            workInProgress,
            nextChildren,
            renderExpirationTime,
        );
    }

    workInProgress.memoizedState = instance.state;

    if (hasContext) {
        invalidateContextProvider(workInProgress, Component, true);
    }

    return workInProgress.child;
}

export function reconcileChildren(
    current: Fiber | null,
    workInProgress: Fiber,
    nextChildren: any,
    renderExpirationTime: ExpirationTime,
) {
    if (current === null) {
        // Mount components
        workInProgress.child = mountChildFibers(
            workInProgress,
            null,
            nextChildren,
            renderExpirationTime,
        );
    } else {
        // Update componentsworkInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderExpirationTime, ); }}Copy the code

Update workinprogress. child workinprogress. child workinprogress. child workinprogress. child workinprogress. child

In fact, mountChildFibers and reconcileChildFibers both point to the same function for reconcileChildFibers. The difference is the second parameter currentFirstChild. If null, a new Fiber object is created, otherwise reuse and update props. For example, the reconcileSingleElement is used to deal with situations where there is only a single node.

completeWork

function completeWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderExpirationTime: ExpirationTime,
) :Fiber | null {
    const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
        / / to omit...
        case HostComponent: {
            popHostContext(workInProgress);
            const rootContainerInstance = getRootHostContainer();
            const type = workInProgress.type;
            // Whether the DOM node corresponding to the fiber node exists
            // update
            if(current ! = =null&& workInProgress.stateNode ! =null) {
                // Calculate the new update Ue for the WIP
                // updateQueue is an array of odd-indexed prop keys and even-indexed prop values
                updateHostComponent(
                    current,
                    workInProgress,
                    type,
                    newProps,
                    rootContainerInstance,
                );

                if (current.ref !== workInProgress.ref) {
                    markRef(workInProgress);
                }
            } else {
                // mount
                if(! newProps) {return null;
                }

                const currentHostContext = getHostContext();
                // Is it server rendering
                let wasHydrated = popHydrationState(workInProgress);
                if (wasHydrated) {
                    / / to omit...
                } else {
                    // Generate the real DOM
                    let instance = createInstance(
                        type,
                        newProps,
                        rootContainerInstance,
                        currentHostContext,
                        workInProgress,
                    );

                    // Insert the descendant DOM node into the newly generated DOM node and form an off-screen DOM tree from bottom to top
                    appendAllChildren(instance, workInProgress, false.false);

                    workInProgress.stateNode = instance;

                    // processing props similar to updateHostComponent
                    if( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }}if(workInProgress.ref ! = =null) { markRef(workInProgress); }}return null;
        }
        / / to omit...}}Copy the code

First, as with beginWork, check whether current === NULL is mount or update.

Update, mainly do the following things, specific source code diffProperties:

  • Calculate the newSTYLE prop
  • Calculate the newDANGEROUSLY_SET_INNER_HTML prop
  • Calculate the newCHILDREN prop

Each time a new prop is computed, its propKey is stored in the array updatePayload paired with nextProp. Finally, assign updatePayload to wip.updatequeue.

When mounting, there are many things to deal with, which are roughly as follows:

  • createInstance: in order toFiberThe node generates the corresponding truthDOMnode
  • appendAllChildren: the childrenDOMInsert the newly generated nodeDOMNodes. To form the whole from the bottom upDOMThe tree
  • finalizeInitialChildrenIn:setInitialPropertiesHandles event registration in. insetInitialDOMPropertiesAccording to thepropsInitialize theDOMattribute

Value of note is the appendAllChildren method. Because completeWork is an upward traceback process, each appendAllChildren call inserts the generated descendant DOM node under the currently generated DOM node. When you go back to the root node, the entire DOM tree is updated.

effectList

After each completeWork, a node is processed. As mentioned earlier, the Reconciler attaches effecttags to the changed nodes, which are used to update the Reconciler in the Renderer based on the execution of the node’s EffectTags.

So in the upper function of completeWork, completeUnitOfWork (the code omitted earlier), the completeWork maintains a one-way linked list of effectLists after each execution. Insert the linked list if the current Fiber has an effectTag.

If (returnFiber! == null && (returnFiber. EffectTag & Incomplete) === NoEffect) {// firstEffect is the head of the linked list if (returnFiber. FirstEffect === = null) { returnFiber.firstEffect = workInProgress.firstEffect; } / / lastEffect for linked list node if (workInProgress lastEffect! == null) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } const effectTag = workInProgress.effectTag; // Skip NoWork and PerformedWork tags. The latter is provided to React Tools to read if (effectTag > PerformedWork) {if (returnFiber. LastEffect! == null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; }}Copy the code

This brings the Reconciler process to an end. Look back at the beginning of the summary, is it clear a few ~

Renderer(Commit)

The code in the Commit phase is relatively simple compared to the other two. Its entry in the previous analysis of task scheduling entrance end of performConcurrentWorkOnRoot finishConcurrentRender function. The final function called is commitRootImpl. Take a look at the code:

let nextEffect: Fiber | null = null;

function commitRootImpl(root, renderPriorityLevel) {
    / / to omit...
    const finishedWork = root.finishedWork;
    const expirationTime = root.finishedExpirationTime;
    if (finishedWork === null) {
        return null;
    }
    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;

    // commit Cannot be interrupted. Always done synchronously.
    // Therefore, these can now be cleared to allow a new callback to be scheduled.
    root.callbackNode = null;
    root.callbackExpirationTime = NoWork;
    root.callbackPriority = NoPriority;
    root.nextKnownPendingLevel = NoWork;
    / / to omit...

    / / get effectList
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) {
        if(finishedWork.lastEffect ! = =null) {
            finishedWork.lastEffect.nextEffect = finishedWork;
            firstEffect = finishedWork.firstEffect;
        } else{ firstEffect = finishedWork; }}else {
        firstEffect = finishedWork.firstEffect;
    }

    if(firstEffect ! = =null) {
        / / to omit...
        nextEffect = firstEffect;
        do {
            // [Q]: Execute snapshot = getSnapshotBeforeUpdate()
            / / the assignment for Fiber. StateNode. Instance. __reactInternalSnapshotBeforeUpdate = the snapshot
            commitBeforeMutationEffects();
        } while(nextEffect ! = =null);
        / / to omit...
        nextEffect = firstEffect;
        do {
            // [Q]: Add, delete and modify DOM operations according to Fiber. EffectTag
            // componentWillUnmount() is also called if the component is unmounted
            commitMutationEffects(root, renderPriorityLevel);
        } while(nextEffect ! = =null);
        / / to omit...
        nextEffect = firstEffect;
        do {
            // [Q]: life cycle after render
            // current === null ? componentDidMount : componentDidUpdate
            commitLayoutEffects(root, expirationTime);
        } while(nextEffect ! = =null);
        stopCommitLifeCyclesTimer();

        nextEffect = null;

        // Tells the Scheduler to stop scheduling at the end of the frame so that the browser has a chance to draw.
        requestPaint();
        / / to omit...
    } else {
        / / to omit...
    }
    / / to omit...
    return null;
}
Copy the code

A lot of code is omitted, leaving the main content. The main logic is to take the Reconciler’s effectList and run it through it three times, doing the following:

  1. To obtainSnapsshot; Used forcomponentDidUpdateThe third parameter of
  2. According to theFiber.effectTagPerform specific operations on the component or DOM
  3. Call the lifecycle functions of all components

commitBeforeMutationEffects

See commitBeforeMutationLifeCycles complete code, the components of tai for ClassComponent mainly logic is as follows:

const current = nextEffect.alternate;
finishedWork = nextEffect;
if (finishedWork.effectTag & Snapshot) {
    if(current ! = =null) {
        const prevProps = current.memoizedProps;
        const prevState = current.memoizedState;
        const instance = finishedWork.stateNode;
        constsnapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); instance.__reactInternalSnapshotBeforeUpdate = snapshot; }}Copy the code

commitMutationEffects

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & ContentReset) {
            // Set the literal content of the node to an empty string
            commitResetTextContent(nextEffect);
        }

        if (effectTag & Ref) {
            const current = nextEffect.alternate;
            if(current ! = =null) {
                // if ref is set to null, the ref will be set later, so the previous values on ref need to be cleared firstcommitDetachRef(current); }}let primaryEffectTag =
            effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            case Placement: {
                commitPlacement(nextEffect);
                // Remove Placement tags from effectTag
                nextEffect.effectTag &= ~Placement;
                break;
            }
            case PlacementAndUpdate: {
                // Placement
                commitPlacement(nextEffect);
                nextEffect.effectTag &= ~Placement;

                // Update
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Deletion: {
                // componentWillUnmount
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break;
            }
            / / to omit...} nextEffect = nextEffect.nextEffect; }}Copy the code

I don’t think there’s much to talk about. Note that commitDetachRef is called to clear the ref reference before starting. Different DOM operations are then performed for different effectTags.

  • commitPlacement; A node is added. The calculation algorithm of node insertion position can be seen below.
  • commitWork; According to theReconcilerindiffPropertiescalculatedupdateQueuearrayDOMupdate
  • commitDeletion; This step calls each component under the subtree from the top downcomponentWillUnmountfunction

commitLayoutEffects

function commitLayoutEffects(root: FiberRoot, committedExpirationTime: ExpirationTime,) {
    while(nextEffect ! = =null) {
        setCurrentDebugFiberInDEV(nextEffect);

        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            recordEffect();
            const current = nextEffect.alternate;
            commitLayoutEffectOnFiber(
                root,
                current,
                nextEffect,
                committedExpirationTime,
            );
        }

        if(effectTag & Ref) { recordEffect(); commitAttachRef(nextEffect); } resetCurrentDebugFiberInDEV(); nextEffect = nextEffect.nextEffect; }}function commitLifeCycles(
    finishedRoot: FiberRoot,
    current: Fiber | null,
    finishedWork: Fiber,
    committedExpirationTime: ExpirationTime,
) :void {
    switch (finishedWork.tag) {
        // ...
        case ClassComponent: {
            const instance = finishedWork.stateNode;
            if (finishedWork.effectTag & Update) {
                if (current === null) {
                    instance.componentDidMount();
                } else {
                    const prevProps =
                        finishedWork.elementType === finishedWork.type
                            ? current.memoizedProps
                            : resolveDefaultProps(finishedWork.type, current.memoizedProps);
                    constprevState = current.memoizedState; instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); }}const updateQueue = finishedWork.updateQueue;
            if(updateQueue ! = =null) {
                // Call the setState registered callback function
                commitUpdateQueue(finishedWork, updateQueue, instance);
            }
            return;
        }
        // ...}}Copy the code

Again, traverse each Fiber node. If it’s ClassComponent, call the lifecycle method. For the updated ClassComponent, we need to determine if the setState called has a callback, and if so, we need to call it here as well. Finally, commitAttachRef is called to update the REF reference.

This is the end of the Commit phase.

To be honest, the React source code is really a lot. It takes a lot of time and effort to analyze each point in full detail. This paper only analyzes a general process, many details are not analyzed in place. I’ll spend some time exploring some of the details later. In the final analysis, it is only from surface to surface, not from surface to point analysis. Many views are personal understanding, write out for learning exchange, there is something wrong, please also put forward suggestions.