React16 source code parsing - ExpirationTime

Posted by imurkid on Sat, 19 Oct 2019 00:39:59 +0200

Welcome to read the React source code analysis series:
React16 source code analysis (I) - illustration of Fiber architecture
React16 source code analysis (2) - create update
React16 source code analysis (III) - ExpirationTime
React16 source code analysis (IV) - Scheduler
React16 source code analysis (V) - update process rendering stage 1
React16 source code analysis (VI) - update process rendering stage 2
React16 source code analysis (7) - update process rendering stage 3
React16 source code analysis (VIII) - update process submission stage
Updating...

In my last article, the updateContainer function in the ReactDOM.render process has a function to calculate the expiration time:

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  // Here, the currentTime and the current Fiber object are passed in to call the function to calculate the expirationTime.
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

In the setState and forceUpdate discussed in the previous article, we also need to calculate the expirationTime. This article will analyze how the expirationTime is calculated.

Why do I need ExpirationTime?

The most exciting change brought about by react 16 is the Fiber architecture, which has changed the component rendering mechanism of react. The new architecture enables the components that used to render synchronously to be asynchronized, interrupt rendering halfway, and perform higher priority tasks. Release the browser main thread.

So every task will have a priority, otherwise it will be a mess.... ExpirationTime is the priority, it is an expiration time.

computeExpirationForFiber

Before calling ExpirationTime, requestCurrentTime was called to get a currentTime. This function involves some complex logic about the later knowledge. We will understand it as a concept similar to the current time before we go into it.

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
    // ......
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // Updates caused by interaction 
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // Normal asynchronous update
        expirationTime = computeAsyncExpiration(currentTime);
      }
    } 
    // ......
  }
  // ......
  return expirationTime;
}

In asynchronous update, we see two ways to calculate update. computeInteractiveExpiration and compueasyncexpiration

computeInteractiveExpiration

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,//150
    HIGH_PRIORITY_BATCH_SIZE,//100
  );
}

computeAsyncExpiration

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,//5000
    LOW_PRIORITY_BATCH_SIZE,//250
  );
}

computeExpirationBucket

Looking at the above two methods, we find that they call the same method: computeExpirationBucket, but the parameters passed in are different, and the constants are passed in. The incoming of compute interactive expiration is 150, 100, and the incoming of compute async expiration is 5000, 250. Indicates that the priority of the former is higher. So I call the former high priority update and the latter low priority update.

Let's see the details of the computeExpirationBucket method:

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;

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

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

After reading it, I was dazed. What is it doing? Don't worry, let's sort out the formula:
Take the low priority update as an example. The final formula is: ((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25

Only currentTime is a variable.

We can try several more values:

((((101 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 600
((((102 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 625
((((105 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 625
((((122 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 625
((((126 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 625
((((127 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 // 650

In short, the final result is increased by 25. For example, if we input 102 - 126, the final result is 625, but by 127, the result is 650, which is the effect of dividing by 25.
That is to say, the interval of expirationTime for low priority updates is 25ms, which smoothes the error of calculating the expiration time within 25ms. React allows two similar updates (within 25ms) to get the same expiration time for update. The purpose is to automatically merge the two updates into one update, so as to achieve batch update.

Note: if you try multiple sets of data with high priority update, you will find that the interval of expirationTime is 10ms.

No matter how complicated the world is, it still remains lovely.
I'm the grapefruit fairy. If there is something wrong with the article, please correct it.~

Topics: Javascript React