Interpretation of React source code [2] update and creation

Posted by torrentmasta on Fri, 13 Dec 2019 18:21:05 +0100

May your future be pure and bright, like your lovely eyes at the moment. ——Pushkin

We have explored the origin of species, and we have imagined where to go in the future. A program, it also has life and death, more than reincarnation.

The fresh life of React originated from React dom.render. This process will reserve many necessities for its life. We follow this clue to explore the joy of infant React application at the beginning of its birth.

The operation of update creation is summarized as the following two scenarios

  • ReactDOM.render
  • setState
  • forceUpdate

ReactDom.render

The content is connected in series with a diagram to cover it.

First of all, we see the definition of react DOM in react DOM / client / react DOM, including well-known methods, unstable methods and methods to be discarded.

const ReactDOM: Object = {
  createPortal,

  // Legacy
  findDOMNode,
  hydrate,
  render,
  unstable_renderSubtreeIntoContainer,
  unmountComponentAtNode,

  // Temporary alias since we already shipped React 16 RC with it.
  // TODO: remove in React 17.
  unstable_createPortal(...args) {
    // ...
    return createPortal(...args);
  },

  unstable_batchedUpdates: batchedUpdates,

  flushSync: flushSync,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    // ...
  },
};

The methods here are all from. / ReactDOMLegacy. The definition of render method is very simple. As we often use, the first parameter is the component, the second parameter is the DOM node to be mounted by the component, and the third parameter is the callback function.

export function render(
  element: React$Element<any>,
  container: DOMContainer,
  callback: ?Function,
) {
  // ...
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // The first rendering does not mark the update as batched
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

It's not hard to find that when we call react dom.render, the returned parentComponent is null, and the first rendering will not update the batch policy, but needs to be completed as soon as possible. (subsequent introduction to batched updates)

From this part of source code, we can see that the connection between the usage of render and createtotal is to create a Root node through the DOM container. The source code is as follows

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): RootType {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      // ...
      container.removeChild(rootSibling);
    }
  }
    
  return createLegacyRoot(
    container,
    shouldHydrate
      ? {
          hydrate: true,
        }
      : undefined,
  );
}

createLegacyRoot is defined in. / ReactDOMRoot. It specifies the created DOM container and some option settings. Finally, a ReactDOMBlockingRoot is returned.

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

function ReactDOMBlockingRoot(
  container: DOMContainer,
  tag: RootTag,
  options: void | RootOptions,
) {
  this._internalRoot = createRootImpl(container, tag, options);
}

function createRootImpl(
  container: DOMContainer,
  tag: RootTag,
  options: void | RootOptions,
) {
  // Tag is either LegacyRoot or Concurrent Root
  // ...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // ...
  return root;
}

The key point is that the method finally calls createContainer to create root, and the method will create the FiberRoot described in the previous section. This object plays a very important role in the subsequent update scheduling process. We will introduce the update scheduling content in detail.

In this section, we see two methods: createContainer and updateContainer, both of which are from real reconciler / inline.dom, and are finally defined in 'real reconciler / SRC / reactfiberreconciler. The creation method is simple, as follows

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

Let's go on and see the updateContainer method, which defines the update related operations. The most important point is expirationTime, which is directly translated into Chinese and is the expiration time. Let's think about why the expiration time is needed here and what is the meaning of the expiration time? How is the expiration time calculated? As we can see further, the computeExpirationForFiber method is used to calculate the expiration time. First, we put the source code fragment here.

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTimeForUpdate();
  // ...
  const suspenseConfig = requestCurrentSuspenseConfig();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  // ...

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

  const update = createUpdate(expirationTime, suspenseConfig);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }

  enqueueUpdate(current, update);
  scheduleWork(current, expirationTime);

  return expirationTime;
}

After the update timeout is calculated, the update object createUpdate is created, and then the element is bound to the update object. If there is a callback function, the callback function is also bound to the update object. After the update object is created, add update to the UpdateQueue. See the previous section for the data structure of update and UpdateQueue. At this point, task scheduling begins.

setState and forceUpdate

These two methods are bound in the file where we originally defined React. They are specifically defined in React / SRC / React BaseClasses, as follows

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

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

That's why it's easy to expand React native on the basis of React, because React just makes some specifications and structure settings. The specific implementation is in React DOM or React native, so as to achieve platform adaptability.

this.setState is used to update Class components. We have been familiar with this api for a long time. For the update creation of object components, the definition is in react reconciler / SRC / reactfiberclasscomponent.js, and the classComponentUpdater object defines the enqueueSetState, enqueueReplaceState and enqueueForceUpdate object methods. Observing these two methods, we will find that the difference is enqueue ReplaceState and enqueueForceUpdate will bind a tag to the created update object to mark whether the type of update is ReplaceState or ForceUpdate. Let's see the code snippet together.

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      // ...
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.tag = ReplaceState;
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      // ...
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      // ...
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
};

We can also find that the implementation of setState update is basically the same as that of react dom.render.

  1. Update expiration time
  2. Create Update object
  3. Bind some properties for the update object, such as tag and callback
  4. Enqueueupdate of the created update object
  5. Enter the scheduling process

Function of expirationTime

expirationTime is used in the scheduling and rendering process of React. It is used to determine the priority. For different operations, there are different response priorities. At this time, we can calculate expirationTime through the currentTime: ExpirationTime variable and the predefined priority EXPIRATION constant. Is currentTime like Date.now() in our bad code? Wrong! This operation will result in frequent calculation and performance degradation, so we define the calculation rules of currentTime.

Get currentTime

export function requestCurrentTimeForUpdate() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // We're inside React, so it's fine to read the actual time.
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}

This method defines how to get the current time. The now method is provided by. / schedulerwithreact integration. It seems that the definition of the now method is not easy to find. We debug the scheduler ﹣ now through the breakpoint, and finally we can find that the acquisition of the time is through window.performance.now(), and then we find that msToExpirationTime is defined in ReactFiberExpirationTime.js expirationTime related processing logic.

Different expirationtimes

Read about react reconciler / SRC / react fiberexpiration time. There are three different ways to calculate the xpiration time: compute async expiration, compute suspend expiration, and compute interactive expiration. The three methods all receive three parameters. The first parameter is the current time obtained above. The second parameter is the agreed timeout. The third parameter is related to the granularity of batch update.

export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;

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

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

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

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

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

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

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

export function computeSuspenseExpiration(
  currentTime: ExpirationTime,
  timeoutMs: number,
): ExpirationTime {
  // TODO: Should we warn if timeoutMs is lower than the normal pri expiration time?
  return computeExpirationBucket(
    currentTime,
    timeoutMs,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

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

The focus is on the definition of ceil method. The method passes two parameters, the number to be evaluated and the expected precision. You may as well bring in two values at will to observe the function. number = 100, precision = 10, then the return value of the function is (((100 / 10) | 0) + 1) * 10. We keep the precision value unchanged. Changing number will find that when our value is between 100-110, the The function returns the same value. Oh It is suddenly realized that the original method is to ensure that updates in the same bucket get the same expiration time, so that the creation of updates in a short time interval can be combined.

The processing of timeout is very complex. In addition to the expirationTime we see, there are childExpirationTime, root.firstPendingTime, root.lastExpiredTime, root.firstSuspendedTime and root.lastSuspendedTime. The related properties under root mark the order of expirationTime of the sub node fiber, which constitutes the order of processing priority. Childexpirationtim E is to update its childExpirationTime value to the child node expirationTime when traversing the subtree.

The above is the core process for React to create updates. We'll see you later in the next chapter. Don't forget to collect WeChat public numbers, B stations and nuggets.

I'm oneness, hero goodbye!

Topics: Javascript React github Fragment