This is my fourth article dissecting React source code. The previous articles are all specific dissecting code, but I think this approach may not be too good. So starting with this article, I’m going to write a separate article about what I learned in the source code, which might be more reader friendly.

Article related Information

  • React 16.8.6 React 16.8.6 React 16.8.6 React 16.8.6 React 16.8.6 React 16.8.6 React 16.8.6
  • A collection of all previous articles

Why is scheduling needed?

We all know that JS and rendering engines are mutually exclusive. If JS is executing code, rendering engine work will stop. If we have a complex composite component that needs to be rerendered, the call stack may be very long

Scheduling is designed to solve the problem of an excessively long call stack, which, combined with complex operations in the middle, can block the rendering engine for long periods of time and cause a poor user experience.

React assigns a context time to a task with a higher priority before the expiration date. High-priority tasks can interrupt low-priority tasks (so some lifecycle functions are executed multiple times). This allows you to compute updates in segments (i.e., time sharding) without affecting the user experience.

React How to implement scheduling

React implements scheduling with two components:

  1. Calculate the expriationTime of the task
  2. Implement a polyfill version of requestIdleCallback

Next, let the author introduce two pieces of content for everyone.

expriationTime

The function of expriationTime has been briefly introduced in the previous section. This time can help us compare the priorities of different tasks and calculate the timeout of tasks.

So how did this time figure out?

The current time refers to performance.now(), which returns a millisecond accurate timestamp (although not highly accurate), and not all browsers are compatible with the Performance API. If date.now () is used, the accuracy will be worse, but for convenience, we will consider the current time to be performance.now().

A constant is a value derived from different priorities. React has five priorities:

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
Copy the code

Their corresponding values are different, and the details are as follows

var maxSigned31BitInt = 1073741823;

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = - 1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
Copy the code

That is, suppose the current time is 5000 and there are two tasks with different priorities to execute. The former belongs to the ImmediatePriority and the latter belongs to UserBlockingPriority, so the calculated time of the two tasks are 4999 and 5250 respectively. This time can be used to figure out whose priority is higher by comparing the size, or to get the timeout of the task by subtracting the current time.

requestIdleCallback

Having said expriationTime, the next topic is to implement requestIdleCallback. Let’s first look at what this function does

The function’s callback methods are called sequentially during the browser’s idle period, allowing us to perform tasks in an event loop without affecting delay-sensitive events such as animations and user interactions.

We can also see in the figure above that the callback method is executed after rendering. So, having introduced functions, let’s talk about compatibility.

This function is not very compatible, and it has a fatal flaw:

requestIdleCallback is called only 20 times per second – Chrome on my 6×2 core Linux machine, it’s not really useful for UI work.

That means requestIdleCallback can only call callbacks 20 times a second, which isn’t enough for the current situation, so the React team decided to implement the function themselves.

If you want to learn more about replacing requestIdleCallback, you can read Issus.

How to implement requestIdleCallback

The core of the requestIdleCallback function is just one thing: how do you call the callback multiple times when the browser is idle and after rendering?

When it comes to multiple executions, you definitely need to use timers. RequestAnimationFrame is the only timer with any degree of accuracy, so requestAnimationFrame is the next step in implementing requestIdleCallback.

RequestAnimationFrame’s callback method is executed before each redraw, and it also has one flaw: the callback does not execute while the page is in the background, so we need to remedy this

rAFID = requestAnimationFrame(function(timestamp) {
  // cancel the setTimeout
  localClearTimeout(rAFTimeoutID);
  callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
  // Timing 100 ms is a best practice
  localCancelAnimationFrame(rAFID);
  callback(getCurrentTime());
}, 100);
Copy the code

When a requestAnimationFrame does not execute, there is a setTimeout to remedy this, and both timers can cancel each other internally.

With requestAnimationFrame we’ve only done this a few times, so how do we know if the browser is currently free?

Everyone knows that within a frame, the browser may respond to user interaction events, execute JS, and render a series of computational draws. Assuming that our current browser supports 60 frames per second, that means a frame is 16.6 milliseconds. If these actions exceed 16.6ms, the render will not be completed and frames will drop, which will affect the user experience. If none of the above actions took 16.6 milliseconds, we assume that there is free time for us to perform the task.

So next we need to figure out if the current frame has any time left for us to use.

let frameDeadline = 0
let previousFrameTime = 33
let activeFrameTime = 33
let nextFrameTime = performance.now() - frameDeadline + activeFrameTime
if (
  nextFrameTime < activeFrameTime &&
  previousFrameTime < activeFrameTime
) {
  if (nextFrameTime < 8) {
    nextFrameTime = 8;
  }
  activeFrameTime =
    nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
  previousFrameTime = nextFrameTime;
}
Copy the code

The core of this code is to figure out how long each frame takes and how long the next frame takes. In simple terms, if the current time is 5000 and the browser supports 60 frames, then one frame is approximately 16ms and the next frame is 5016.

After determining the time of the next frame, we simply compare the current time to whether the time of the next frame is less than the time of the next frame, so that we can clearly know whether there is free time to perform tasks.

The final step is how to perform the task after rendering. This is where the knowledge of event loops comes in

You probably know the difference between microtasks and macro tasks, so I won’t bother with that part here. As you can see from the image above, only the macro task is executed first after rendering, so the macro task is the operation to implement this step.

However, there are many ways to generate a macro task and each of them has its own priority, so in order to execute the task fastest, we must choose the high priority method. Here we have chosen MessageChannel for this task and not setImmediate because of poor compatibility.

So far, requestAnimationFrame + calculating the frame time and the next frame time + MessageChannel are the three key points for implementing requestIdleCallback.

Scheduling process

Having said so much above, we will comb through the whole scheduling process in this section.

  • First, each task has its own priority, which can be calculated by adding the current time and the constant corresponding to the priorityexpriationTime.High-priority tasks interrupt low-priority tasks
  • Determine the current task before schedulingIs late, expired without scheduling, direct callport.postMessage(undefined), so that expired tasks can be executed immediately after rendering
  • If the task has not expired, passrequestAnimationFrameStart the timer and call the callback method before redrawing
  • In the callback method we first needCalculate the time of each frame and the time of the nextAnd then executeport.postMessage(undefined)
  • channel.port1.onmessageWill be called after rendering, and in this process we first need to determineWhether the current time is less than the next frame time. If it’s less than that, it means we have free time to perform tasks. If it is greater than that, it means that the current frame has no free time, at which point we need to determine if any task is expired,If it’s overdue, you still have to perform this task. If not, the task can be pushed to the next frame to see if it can be executed

Scheduling won’t just be owned by React

In the future, scheduling won’t just be available with React. React has tried to separate the scheduling module into a library that can be incorporated into your own applications in the future to improve user experience.

There is also a proposal from the community for the browser to be built with functionality in this area. You can read this library.

The last

Reading the source code can be a tedious process, but the benefits can be huge. If you have any questions along the way, please feel free to let me know in the comments section.

Also, writing this series is a time-consuming project, including maintaining code comments, making the article as readable as possible, and finally drawing. If you think the article looks good, please give it a thumbs up.

Finally, feel the content is helpful can pay attention to my public number “front-end really fun”, there will be a lot of good things waiting for you.