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

APP promotion: 95% of people use these 5 conversion strategies incorrectly

The conversion rate optimization strategy is inef...

How to make a good online event promotion plan?

In marketing psychology, herd mentality, the ment...

Do women snore, too? And more than men? The reason is revealed!

Audit expert: Wu Xi Associate Chief Physician, De...

7 tips to teach you how to write a title that catches readers' attention

Advertising guru David Ogilvy once expressed his ...

High-quality case studies on information flow delivery in the automotive industry

It’s the end of the year and it’s the crazy car b...

How to write code that others can understand

With the continuous development of the software i...

Himalaya FM audio traffic promotion plan

Himalaya FM is a well-known audio sharing platfor...

GM may be the first company to test self-driving cars in New York

The rapid iteration of intelligent technology has...

60-day SEO training course to build millions of traffic

This course is suitable for practitioners who hav...