Writing a JavaScript framework: better timed execution than setTimeout

Writing a JavaScript framework: better timed execution than setTimeout

[[177395]]

This is the second chapter of the series on JavaScript frameworks. In this chapter, I am going to talk about different ways of executing asynchronous code in the browser. You will learn about the differences between timers and event loops, such as setTimeout and Promises.

This series is about an open source client-side framework called NX. In this series, I will mainly explain the main difficulties that had to be overcome to write this framework. If you are interested in NX you can visit our homepage.

This series contains the following chapters:

  1. Project Structure
  2. Scheduled execution (current chapter)
  3. Sandbox code evaluation
  4. Introduction to Data Binding
  5. Data Binding and ES6 Proxies
  6. Custom Elements
  7. Client-side routing

Asynchronous code execution

You may be familiar with Promises, process.nextTick(), setTimeout(), and perhaps requestAnimationFrame() as ways to execute code asynchronously. They all use the event loop internally, but they differ in their precise timing.

In this chapter, I'll explain the differences between them and then show you how to implement a timing system in an advanced framework like NX. Rather than reinventing the wheel, we'll use the native event loop to achieve our goals.

Event Loop

The event loop is not even mentioned in the ES6 specification. JavaScript itself only has tasks and job queues. More complex event loops are defined in the NodeJS and HTML5 specifications respectively. Since this article is for the front-end, I will explain the latter in detail.

The event loop can be thought of as a conditional loop. It keeps looking for new tasks to run. One iteration of this loop is called a tick. The code executed during a tick is called a task.

  1. while (eventLoop.waitForTask()) {
  2. eventLoop.processNextTask()
  3. }

Tasks are synchronous code that schedules other tasks in a loop. A simple way to call a new task is setTimeout(taskFn). However, tasks can come from many sources, such as user events, network or DOM operations.

[[177396]]

Task Queue

To make things a bit more complicated, an event loop can have multiple task queues. There are two constraints here, events from the same task source must be in the same queue, and tasks must be processed in the order they were inserted. Other than that, the browser can do whatever it wants. For example, it can decide which task queue to process next.

  1. while (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. }

With this model, we cannot precisely control the timing. If we use setTimeout(), the browser may decide to run our queue after several other queues have finished running.

[[177397]]

Microtask Queue

Fortunately, the event loop also provides a single queue called the microtask queue. The microtask queue is emptied of tasks every tick when the current task is finished.

  1. while (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. const microtaskQueue = eventLoop.microTaskQueue
  7. while (microtaskQueue.hasNextMicrotask()) {
  8. microtaskQueue.processNextMicrotask()
  9. }
  10. }

The simplest way to call a microtask is Promise.resolve().then(microtaskFn). Microtasks are processed in the order they are inserted, and since there is only one microtask queue, the browser won't mess up the timing.

Additionally, a microtask can schedule new microtasks, which will be inserted into the same queue and processed within the same tick.

[[177398]]

Rendering

*** is the Rendering scheduling, which is different from event processing and decomposition. Drawing is not done in a separate background task. It is an algorithm that can be run at the end of each loop tick.

Here again the browser has a lot of freedom: it may paint after each task, but it may also not paint after hundreds of tasks have been executed.

Luckily, we have requestAnimationFrame(), which executes the passed function before the next draw. Our final event model looks like this:

  1. while (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. const microtaskQueue = eventLoop.microTaskQueue
  7. while (microtaskQueue.hasNextMicrotask()) {
  8. microtaskQueue.processNextMicrotask()
  9. }
  10. if (shouldRender()) {
  11. applyScrollResizeAndCSS()
  12. runAnimationFrames()
  13. render()
  14. }
  15. }

Now let's use what we know to create a timing system!

Utilizing the event loop

Like most modern frameworks, NX is based on DOM manipulation and data binding. Batch operations and asynchronous execution for better performance. For the above reasons, we use Promises, MutationObservers and requestAnimationFrame().

The timer we expect is as follows:

  1. Code from the developer
  2. Data binding and DOM manipulation are performed by NX
  3. Developer-defined event hooks
  4. The browser draws

Step 1

NX registers objects based on ES6 proxies and DOM mutations based on MutationObserver (detailed in the next section). It acts as a microtask delay until step 2 is executed before reacting. This delay has been converted to an object in Promise.resolve().then(reaction), and it will automatically run through the mutation observer.

Step 2

The code (task) from the developer runs to completion. The microtasks registered by NX start executing. Because they are microtasks, they are executed in order. Note that we are still in the same tick loop.

Step 3

Developers tell NX to run the hook via requestAnimationFrame(hook). This may happen after the tick cycle. The important thing is that the hook runs before the next paint and after all data manipulation, DOM and CSS changes have been completed.

Step 4

The browser draws the next view. This may happen after a tick, but never before step 3 of a tick.

Things to keep in mind

We implemented a simple but effective timing system on top of the native event loop. In theory it works well, but it is still very fragile and a slight mistake can lead to a serious bug.

In a complex system, it is important to establish certain rules and keep them in the future. In NX, there are the following rules:

  1. Never use setTimeout(fn, 0) for internal operations
  2. Use the same method to register microtasks
  3. Microtasks are for internal operations only
  4. Don't interfere with developer hook execution time

Rules 1 and 2

Data reflection and DOM operations will be executed in the order of operation. This way, their execution can be delayed well as long as they are not mixed. Mixing execution will cause inexplicable problems.

The behavior of setTimeout(fn, 0) is completely unpredictable. Registering microtasks with different methods can also cause confusion. For example, in the following example, microtask2 will not correctly run before microtask1.

  1. Promise.resolve(). then () .then (microtask1)
  2. Promise.resolve(). then (microtask2) [[177399]]

Rules 3 and 4

It is very important to separate the time window of developer code execution and internal operations. Mixing these two behaviors can lead to unpredictable things happening, and it requires developers to understand the internals of the framework. I think many front-end developers have had similar experiences.

in conclusion

If you are interested in the NX framework, you can visit our homepage. You can also find our source code on GIT.

See you in the next section where we’ll discuss sandboxed code execution!

You can also leave us a message.

<<:  20 common usage examples of time and date libraries in Java 8

>>:  Wang Peng of Yihui Zhongmeng: Offline big data - the next outlet for intelligent technological innovation

Recommend

You can edit the Alipay homepage yourself! Long press the icon to sort/delete

[[429822]] Nowadays, personalized customization h...

The designer of the first iPhone pointed out this big flaw of the iPhone

As Apple's fall conference draws closer, leak...

APP Promotion Operation Manual Complete Strategy

Starting from the position of mobile Internet mar...

What exactly is this high-end “brand tone”?

At the last meeting, the boss brought up the conc...

Is it always fun to be on vacation? Not necessarily!

The May Day holiday is coming to an end in the bl...

Zhong Xue Gao Brand Marketing History

When it comes to Zhong Xue Gao , it is definitely...

How to bid and match Google/Baidu SEM keywords?

Bidding and matching methods are the two most imp...

Anniversary event planning tips!

Recently, a friend of mine went for an interview,...