The importance of grand storytelling
Inside loop, outside loop. Internal cycle based domestic and international double cycle baike.baidu.com/item/%E5%9B… Recently I have read two books: Deconstructing Modernization: Speeches of Wen Tiejun and Problems and Countermeasures of Structural Reform in China’s Economy (huang Qifan). Double cycle, “paradigm design”, “structural design”, grand narrative ability and so on these words always linger. See big men put some complicated things, put aside the limitation of ideology, research is very transparent, speak very clearly…… I just think it’s great. It suddenly occurred to me that Scheduler in React also has a dual-loop design. I just want to whip the Scheduler off again. So why is grand storytelling so important? Because it helps you research problems and tell good stories.
task
The data structure of the task is as follows:
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1};Copy the code
The task has ensured the characteristics of the timeline in the data structure. Let’s ask each attribute in triplets: “what”, “why”, “how to use”.
How to build tasks
id
The ID is incremented by the global taskIdCounter when the task is created. TaskIdCounter is an integer that increments from 1, ensuring uniqueness and representing the order in which tasks are created. Tasks have priority, and if both tasks have the same priority through other means, the ID comes into play. In this case, the task created first has a higher priority, because the task created first must be executed first.
callback
This is the specific work of the mission.
priorityLevel
The priorities of the tasks are as follows: ImmediatePriority, UserBlockingPriority, NormalPriority, LowPriority, and IdlePriority. For each priority, different timeouts are defined. The timeout period is the amount of time that has elapsed since a task can be started before the task expires. The mapping between task priorities and timeout duration is as follows:
Task priority | Timeout time (ms) | describe |
---|---|---|
ImmediatePriority | – 1 | The timeout is -1, and the expiration time is shorter than the startup time. Tasks of this priority are scheduled to be executed as soon as they are started. |
UserBlockingPriority | 250 | Why these values? I never found an authoritative explanation. Use an “experience value” to force the explanation. If you have a better explanation, please contact me. |
NormalPriority | 5000 | |
LowPriority | 10000 | |
IdlePriority | 1073741823 | Max 31 bit integer. The max integer size in V8 for 32-bit systems. Math.pow(2, 30) – 1 –> 0b111111111111111111111111111111 These tasks never run out of time, and no other task has a longer expiration time than this one. |
startTime
When creating a task, you can select whether the task needs to be delayed. Delay required: startTime = currentTime + delay(delay > 0). This type of task is called a delayed task and needs to start immediately: startTime = currentTime + 0. Such tasks are called ordinary tasks
expirationTime
ExpirationTime of a task expirationTime = startTime + timeout. How long has the task expired after it started? This needs to be implemented.
sortIndex
This is the field used for task sorting, which we will introduce in the next section in conjunction with the data structure for task storage.
How to store tasks
When creating a task, there are two different types of tasks: delayed tasks and normal tasks. Tasks are stored in two different structures, timerQueue and taskQueue. TimerQueue and taskQueue are both minimal binary heap structures. Earlier versions of React 16 used one-way linked lists for storage tasks. Since the heap was the industry standard implementation of priority queues, the mid – to late-stage versions began to switch to the least binary heap. For more information refer to the boiling point previously posted: juejin.cn/pin/6877006… I’m not going to talk about the least binary heap, but if you’re not familiar with it, I’m going to recommend a book called “The Journey of The Little Grey algorithm”. If the task is a deferred task, it needs to be stored in the timerQueue. The sort field sortIndex of the tasks in timerQueue is set to startTime. That is, the delayed tasks are sorted according to the start time of the smallest to the largest.
If the task is a normal task, it needs to be stored in the taskQueue. In taskQueue, the sort field sortIndex of tasks is set to expirationTime. That is, ordinary tasks are sorted by their expiration time from smallest to largest. If there are equal tasks in timerQueue and taskQueue, the tasks are sorted by their ids. This is why the implementation mechanism of the ID introduced earlier is global taskIdCounter increment. Will the taskIdCounter increment reach its maximum limit? React doesn’t consider this situation. Why is that? The largest integer that javascript can handle is number. MAX_SAFE_INTEGER is 9007199254740991. React wouldn’t have as many tasks to update in a real working scenario
Delayed task transfer machine
Tasks are divided into normal tasks and delayed tasks, and only the normal tasks are really consumed. There is a mechanism for a delayed task to become a normal task. This mechanism is: delay task delay time reached or has passed (startTime <= currentTime). The sortIndex property changes from startTime to expirationTime, which allows you to order normal tasks in a taskQueue. Move from timerQueue to taskQueue to be consumed. This process involves looping and deleting timerqueues, and increasing taskqueues, where the advantage of a minimum binary heap being faster than a unidirectional list can be seen.
Overview of internal and external double circulation
Scheduler has an internal and external double loop design. Inner and outer double circulation inner circulation is given priority to, two inner circulation completed an outer circulation.
What is the outer loop
The outer loop loop is the inner loop. In two different inner loop processes, one outer loop is completed. The outer loop is used in the following scenarios: Scenario 1: A normal task is created, but both the inner and outer loops have been stopped. The new inner loop needs to be pulled to execute the task. Scenario 2: The inner loop exits prematurely and the task remains. The new inner loop needs to be pulled to perform the remaining tasks
What is an inner loop
The inner loop loops through the normal tasks in the taskQueue, executing the callbacks in the tasks. The internal circulation has three main characteristics: eat more, vomit less, can vomit more times.
Outer loop
Earlier versions of React 16 used window.postMessage + requestAnimationFrame to align task execution with frame. In the browser, the task execution time can be 16.7ms(1000/60 Hz) at most. At that time, it was also designed to automatically adjust the frame time from 33ms, allowing for more suitable environments. React 16 was replaced with MessageChannel in mid-to-late versions. The official explanation for why we changed from requestAnimationFrame to MessageChannel is:
- Github.com/facebook/re…
- Github.com/facebook/re…
- Github.com/facebook/re…
- Github.com/facebook/re…
- Github.com/facebook/re…
- Github.com/facebook/re…
- Github.com/facebook/re…
For more information about MessageChannel:
- Developer.mozilla.org/zh-CN/docs/…
- Developer.mozilla.org/zh-CN/docs/…
- Developers.google.com/web/updates…
In short, I switched to MessageChannel because most tasks don’t really need to be aligned with a frame. Use requestAnimationFrame again if you need to use frame in certain scenarios.
The period of the outer circulation
By not using requestAnimationFrame you are automatically eliminating the 16.7ms cycle limit for frames, and designing the yieldInterval as a regular cycle limit. The default value for the yieldInterval is 5ms, which can be updated using the forceFrameRate provided.
forceFrameRate = function(fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
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);
// The browser is 1000/60 = 16.7ms
/ /?? 125?? 1000 / 125 = 8ms
// Default 1000/200 = 5ms
} else {
// reset the framerate
yieldInterval = 5; }};Copy the code
It can be seen that the legal range for FPS is (0, 125). The browser’s 60 Hz is within this range. Vsync questions: What is vsync? www.digitaltrends.com/computing/w… After the yieldInterval is reached, you release control of the main thread, regardless of whether the render is in Vsync.
How to implement the outer loop
MessageChannel has two ports, and as long as one port is bound to a callback function, the callback function can be triggered when the other port sends a message. This is a macro event.
To reduce serialization costs when sending messages, use port.postMessage(null). Turning the sending switch on once the message has been sent prevents the outer loop from being triggered too often, ensuring that each outer loop only pulls a new inner loop. When the callback is fired, define the deadline for the main thread to be busy: Deadline = currentTime + yieldInterval, and then pull the new inner loop. The deadline is used by the inner loop to determine if it should exit prematurely and release control of the main thread. If the inner loop is prematurely exited and the task is not completed, the callback Korean of the MessageChannel port binding is triggered again using port.postMessage(NULL), thus implementing the outer loop.
Inner loop
The inner loop is the taskQueue that stores common tasks. The function of the inner loop is to perform more and more timely the callback mounted under normal tasks without causing the browser to freeze.
Hungry taskQueue is healthy
Once you enter the inner loop at some point, the inner loop does these things in order:
Start by executing the deferred task transfer machine, turning as many of the appropriate deferred tasks into normal tasks as possible, and sorting all the normal tasks in the taskQueue. Then take out the common task definitions as the current task and place them globally. Expect to loop until the taskQueue is empty, but you can exit early.
Sign of hunger: Eat more
As soon as the callback of the current task is complete, the deferred task will be executed again in a timely manner, and as soon as possible, the taskQueue will be loaded with deferred tasks that can be started in timerQueue.
Hunger and thirst performance two: eat do not want to vomit
If the callback of the current task completes, derivative work may also occur, in which the callback is updated. The task cannot be deleted. Otherwise, the task must be deleted in a timely manner.
Hunger and thirst performance three: vomited not a few want to shut up
If the current task is a normal task with no expiration date, you can exit the taskQueue early. TaskQueue is sorted by context Time or taskIdCounter. If the current task is not due, the next task is not due either.
Hunger and thirst performance four: vomit time is limited
If you decide that you should give up control of the main thread while trying to do specific work on the current task so that the browser can perform higher-priority tasks (such as user input), you need to exit the taskQueue early. So how do you decide to give up control of the main thread? The answer is based on the current main thread busy deadline defined in the outer loop. That is, the inner loop only works when the main thread is idle.
Hungry expression five: vomit finished want to start again
If the taskQueue does not exit early, the taskQueue is flushed, and all normal tasks are completed completely within the main thread’s idle time. Find the earliest delayed task in timerQueue. Because the delayed tasks in timerQueue are sorted from smallest to largest by startTime. The earliest delayed task is the first task in the timerQueue. If the deferred task exists, execute the deferred task transfer machine after the delay expires, and then try to continue the outer loop. Call this process throwing up and wanting to start again. If attempts to continue the outer loop fail, the process is repeated.
Healthy performance one: can vomit several times more
If the taskQueue exits prematurely, the tasks that should be completed by the inner loop are still there. Then continue the outer loop and start a new inner loop.
In a word
These signs of hunger and health are meant to consume more tasks in a more timely manner without blocking the main thread for too long.
Ten thousand one starts at timerQueue
If the taskQueue is empty, the newly created task is still part of the timerQueue’s delayed task, which is currently the only one that can be done. So arrange for the task to start again after the particular delay is over. Use setTimeout to implement this arrangement. However, before the delay time, perhaps because a normal task is created, the outer loop is immediately opened, which opens the inner loop, and this arrangement is not needed. So you need to cancel this arrangement before you start the inner loop. Similarly, it is possible to create a new delay task before the delay time, so cancel the old schedule, take the first delay task in timerQueue and use setTimeout to implement a new schedule.
The future of isInputPending
This is testing whether the user input API, the navigator. The scheduling. IsInputPending
- Engineering.fb.com/developer-t…
- Github.com/WICG/is-inp…
YieldInterval yieldInterval yieldInterval yieldInterval yieldInterval yieldInterval The API does not currently have browsing support, so it is very easy to know if the user is typing, which makes it easier and more timely to release control of the main thread.