This is the second chapter of the JavaScript framework series. In this chapter, I plan to explain how asynchronous code is executed in a browser. You will understand the differences between the timer and the event loop, such as setTimeout and Promises. This series is about an open-source client framework called NX. In this series, I mainly explain the main difficulties that the framework has to overcome. This is the second chapter of the JavaScript framework series. In this chapter, I plan to explain how asynchronous code is executed in a browser. You will understand the differences between the timer and the event loop, such as setTimeout and Promises.
This series is about an open-source client framework called NX. In this series, I mainly explain the main difficulties that the framework has to overcome. If you are interested in NX, visit our homepage.
This series contains the following chapters:
Project Structure
Scheduled execution (current chapter)
Sandbox code Evaluation
Data Binding
Data Binding with ES6 proxy
Custom Elements
Client route
Asynchronous Code Execution
You may be familiar with asynchronous code execution methods such as Promise, process. nextTick (), setTimeout (), and requestAnimationFrame. They all use event loops internally, but they differ in exact timing.
In this chapter, I will explain the differences between them and demonstrate how to implement a timing system in an advanced framework like NX. We don't need to redo it. We will use the native event loop to achieve our goal.
Event Loop
Event loops are not even mentioned in ES6 standards. JavaScript only has jobs and Job queues ). More complex event loops are defined separately in NodeJS and HTML5 specifications, because this article is for the frontend, And I will describe the latter in detail.
Event loops can be seen as loops of a condition. It keeps searching for new tasks to run. An Iteration in this loop is called a tick ). The Code executed during a tick answer is called a task ).
while (eventLoop.waitForTask()) { eventLoop.processNextTask() }
A task is a synchronization code that can schedule other tasks in a loop. SetTimeout (taskFn) is a simple way to call a new task ). In any case, a task may have many sources, such as user events, network operations, or DOM operations.
Task queue
More complex, event loops can have multiple task queues. There are two constraints: the events of the same task source must be in the same queue, and the tasks must be processed in the order of insertion. In addition, the browser can do anything it wants to do. For example, it can decide which task queue to process next.
while (eventLoop.waitForTask()) { const taskQueue = eventLoop.selectTaskQueue() if (taskQueue.hasNextTask()) { taskQueue.processNextTask() } }
With this model, we cannot accurately control timing. If you use the setTimeout () browser, you may decide to run several other queues before running our queue.
Microtask queue
Fortunately, the event loop also provides a single queue called a microtask queue. At the end of the current task, the microtask queue clears the tasks in each tick answer.
while (eventLoop.waitForTask()) { const taskQueue = eventLoop.selectTaskQueue() if (taskQueue.hasNextTask()) { taskQueue.processNextTask() } const microtaskQueue = eventLoop.microTaskQueue while (microtaskQueue.hasNextMicrotask()) { microtaskQueue.processNextMicrotask() } }
The simplest way to call a microtask is Promise. resolve (). then (microtaskFn ). Micro-tasks are processed in the insert order, and because only one micro-task queue exists, the browser will not mess up the time.
In addition, a micro-task can schedule a new micro-task, which will be inserted into the same queue and processed in the same answer.
Draw Rendering
The last step is to draw Rendering scheduling. Unlike event processing and decomposition, Rendering is not completed in a separate background task. It is an algorithm that can run at the end of each loop.
Here, the browser has a lot of freedom: It may be drawn after each task, but it may not be drawn after hundreds of tasks are executed.
Fortunately, we have requestAnimationFrame (), which executes the passed function before the next draw. Our final event model is like this:
while (eventLoop.waitForTask()) { const taskQueue = eventLoop.selectTaskQueue() if (taskQueue.hasNextTask()) { taskQueue.processNextTask() } const microtaskQueue = eventLoop.microTaskQueue while (microtaskQueue.hasNextMicrotask()) { microtaskQueue.processNextMicrotask() } if (shouldRender()) { applyScrollResizeAndCSS() runAnimationFrames() render() } }
Now we use all our knowledge to create a timing system!
Use event Loops
Like most modern frameworks, NX is also based on DOM operations and data binding. Batch operation and asynchronous execution to achieve better performance. For the above reasons, we use Promises, MutationObservers, and requestAnimationFrame ().
The expected timer is as follows:
The Code comes from developers.
Data Binding and DOM operations are performed by NX.
Developer-defined event hooks
Draw in the browser
Step 1
The NX register object runs synchronously Based on the ES6 proxy and DOM changes based on MutationObserver (Change observer) (details in the next section ). It acts as a microtask and is delayed until step 2 is executed. This latency has been converted to objects in Promise. resolve (). then (reaction) and will be automatically run through the change observer.
Step 2
The Code (task) from the developer is completed. Microtask Registration starts with NX. Because they are micro tasks, they are executed in order. Note that we are still in the same tick loop.
Step 3
The developer notifies NX to run the hook through requestAnimationFrame (hook. This may happen after the tick-Answer loop. It is important that the hook runs before the next draw and after all data operations, and DOM and CSS changes have been completed.
Step 4
The browser draws the next view. This may also happen after the tick-Answer loop, but it will never happen before step 3 of a tick-answer.
Keep in mind
We have implemented a simple and effective Timing System Based on the native event loop. Theoretically speaking, it runs well, but it is still very fragile. A slight error may cause a very serious BUG.
In a complex system, the most important thing is to establish certain rules and maintain them later. There are the following rules in NX:
Never use setTimeout (fn, 0) for internal operations
Use the same method to register a micro-task
Micro-tasks are for internal operation only
Do not interfere with the developer hook running time
Rules 1 and 2
Data reflection and DOM operations are performed in the order of operations. In this way, as long as they are not mixed, their execution can be well delayed. Mixed execution may cause inexplicable problems.
SetTimeout (fn, 0) behavior is completely unpredictable. Using different methods to register a microtask can also lead to confusion. For example, in the following example, microtask2 does not run correctly before microtask1.
Promise.resolve().then().then(microtask1) Promise.resolve().then(microtask2)
It is very important to separate the developer's code execution and internal operation time window. Mixing these two behaviors can lead to unpredictable events and requires developers to understand the internal framework. I think many front-end developers have had similar experiences.