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

Russian beauty shouting video online production, online order, sincerely recruit

We all know that Russia is a place rich in beauti...

Is radiation in daily life harmful to the human body?

Radiation is always a concern. But in fact, it is...

Marketing activity planning ideas and misunderstandings!

What kind of marketing activities will be more ef...

By the way, why did BlackBerry choose Indonesia as its last market?

Last month, BlackBerry announced that it would no...

What are the advantages of SEM marketing? How to analyze it?

Everyone knows that there are many ways of online...

Danmu enters TV: the "fleeting love" between the Internet and TV

For many people born in the 1980s, the word "...

Why are iPhone 6 and iPhone 6+ so easy to bend?

As someone who has worked on mobile phone structu...

This drone-shot video is a bit scary

The Verge editor Ben Popper recently received an ...

Can Win10 live up to the title of "the best Windows ever"?

On the evening of July 29, Microsoft officially r...