About the author: Nekron Ant Financial · Data Experience Technology team

sequence

For a long time, my cognitive definition of Event Loop has been knowable but unknown, so I only kept the superficial concept and never really learned it until I read this article — This time, Thoroughly Understand the JavaScript Execution Mechanism. The author of the article was very friendly and started with the smallest example, which I learned a lot from, but the last example involved the difference between How Chrome and Node run, which I was curious about and felt I needed to learn about.

Due to the above reasons, this article was born. Originally, I planned to expand it into three parts: specification, implementation and application. However, it is a pity that due to my poor knowledge of how to imagine application scenarios based on the characteristics of Event Loop, THERE is really no output. As a result, the relevant application is too small, so it is not reflected in the title.

(All the code in this article only runs on Node V8.9.4 and Chrome V63)

PART 1: Specification

Why an Event Loop?

Since Javascript was originally designed as a single-threaded language, Event Loop was developed to prevent blocking of the main thread.

Quiz (1)

So let’s take a look at a piece of code, and what does it print?

console.log(1)

setTimeout((a)= > {
  console.log(2)},0)

Promise.resolve().then((a)= > {
	console.log(3)
}).then((a)= > {
	console.log(4)})console.log(5)
Copy the code

For those unfamiliar with Event Loop, I tried the following analysis:

  1. First of all, we first exclude asynchronous code, the first synchronous execution of the code to find out, you can know that the first print must be1, 5
  2. But do setTimeout and Promise have priority? Or the order of execution?
  3. Also, will setTimeout be inserted between the Promise’s multilevel THEN?

Confused, I tried to run the code, and the correct results were: 1, 5, 3, 4, 2.

So why is that?

define

It seems that we need to start from the specification definition, so look up the HTML specification, the specification is really detailed (LUO) fine (SUO), I will not paste, refining down the key steps are as follows:

  1. Execute the oldest task (once)
  2. Check for microTasks and keep executing until the queue is empty (multiple times)
  3. Perform render

Good guy, I still don’t understand the problem, and suddenly there are two more concepts, task and microtask, which make me even more confused.

After careful reading of the document, I know that these two concepts belong to the classification of asynchronous tasks. Asynchronous tasks registered by different APIS will enter their corresponding queues in turn, and then wait for the Event Loop to push them into the execution stack in turn.

Task includes setTimeout, setInterval, setImmediate, I/O, and UI interaction events

Microtask includes Promise, Process. nextTick, and MutaionObserver

The entire basic Event Loop looks like this:

  • A queue can be thought of as a data structure for storing functions that need to be executed
  • A timer API (setTimeout/setInterval) registers a function to enter the task queue when it expires.
  • The rest of the API registration functions go directly to their respective Task /microtask queues
  • The Event Loop executes once, pulling a task from the task queue
  • The Event Loop continues to check whether the MicroTask queue is empty, executing in turn until the queue is empty

Continue testing (2)

At this time, I looked back at the previous test (1) and found that the concept was very clear. I got the correct answer immediately. I felt lovely and I was no longer afraid of Event Loop

Next, I’m ready for a more difficult challenge (for the article mentioned in the preface, I removed process.nexttick first) :

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})
Copy the code

Analysis is as follows:

  1. Synchronously run code first prints:1, 7,
  2. Next, empty the MicroTask queue:8
  3. The first task executes:2, 4
  4. Next, empty the MicroTask queue:5
  5. The second task executes:9, 11
  6. Next, empty the MicroTask queue:12

Run it under Chrome, all right!

My confidence swelled and I was ready to add process.nextTick to continue testing on Node. I’ll test the first task with the following code:

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})
Copy the code

With previous accumulation, I confidently wrote down the answer: 1, 7, 8, 6, 2, 4, 5, 3.

The correct answer is: 1, 7, 6, 8, 2, 4, 3, 5.

I was confused, but it soon became clear that the function registered by process.nextTick had precedence over the one registered by Promise, so it all made sense

Next, I test the second task:

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

setTimeout(() => {
    console.log(9)
    process.nextTick(() => {
        console.log(10)
    })
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})
Copy the code

This time I have mastered the priorities of microtask, so the answer should be:

  • First task output:One, seven, six, eight, two, four, three, five
  • Then, the second task prints:Nine, 11, 10, 12

However, slap in the face…

The first time I execute it, the output is 1, 7, 6, 8, 2, 4, 9, 11, 3, 10, 5, 12 (that is, the two task executions are mixed together). I went ahead and sometimes printed out the answer I expected.

The reality is really so inexplicable ah! Ah! Ah!

(Oh, sorry, the bleeding can’t stop.) So, what is it??

PART 2: Implementation

As the saying goes:

Specifications are made by people, code is written by people. – anonymous

The spec doesn’t cover all scenarios, and while Chrome and Node are both based on v8 engines, the engines only manage the memory stack, and the apis are designed and implemented by the Runtime itself.

Quiz (3)

Timer is a very important part of the whole Event Loop. Let’s start from Timer to personally experience the differences between specification and implementation.

First, let’s do a little test. What’s the output?

setTimeout(() => {
	console.log(2)
}, 2)

setTimeout(() => {
	console.log(1)
}, 1)

setTimeout(() => {
	console.log(0)
}, 0)
Copy the code

If you look directly at the delay Settings in the code, you will answer: 0, 1, 2.

Other students with some experience might answer: 2, 1, 0. Because the setTimeout documentation for MDN states that the minimum delay for HTML is 4ms:

(Note: The minimum delay is set to give the CPU time to rest.)

In fact, 4MS is specified by the HTML5 spec and is consistent across Browsers released in 2010 and onward. Prior to (Firefox 5.0) / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.

Those of you who have experienced pain will tell you that the answer is: 1, 0, 2. The results are the same for both Chrome and Node.

(Error: After repeated verification, the output order of node is still not guaranteed, node timer is really a mystery ~)

The timer in Chrome

It can be seen from the results of test (3) that the delay effects of 0ms and 1ms are the same. What is the reason behind that? Let’s look at the blink implementation first.

(I didn’t even know how to search where Blink code was hosted. Fortunately, the file name was obvious. It didn’t take long to find the answer.)

(I directly posted the bottom code, if you are interested in the upper code please refer to)

// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp# 93

double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond); 
Copy the code

So interval is the number that was passed in, so you can see that if you pass in 0 and if you pass in 1, you get oneMillisecond, which is 1ms.

That explains why 1ms and 0ms behave the same, but what’s going on with 4ms? I reconfirmed the HTML specification and found that although there is a 4ms limit, there are conditions, see point 11 of the specification:

If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

And, interestingly, the MDN English documentation also conforms to this specification.

I would venture to guess that the HTML5 specification did have a minimum of 4ms at the beginning, but that has been changed in subsequent revisions, and I don’t even rule out that the specification is moving toward implementation, that is, backward impact.

The Node of the timer

Why are the delay effects of 0ms and 1ms the same in Node?

(Or github hosted code looks convenient, directly search to the target code)

/ / https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456

if(! (after >= 1 && after <= TIMEOUT_MAX)) after = 1; // schedule on next tick, follows browser behaviorCopy the code

Comments in the code directly indicate that the minimum 1ms behavior is set to conform to browser behavior.

Event Loop in Node

The above timer is a small episode. Now we return to the core of this paper — Event Loop.

Let’s focus on the implementation of Node, the implementation of BLINK is not expanded in this paper, mainly because:

  • chromeThe behavior so far seems to be consistent with the norm
  • There is not much documentation to refer to
  • Can’t search, don’t know where to find the core code…

(Skip all the research…)

The following figure shows the Event Loop implementation of Node:

Supplementary notes:

  • NodetheEvent LoopBy stage, stage has successively, the order is
    • Expired timers and Intervals: setTimeout/setInterval
    • I/O Events, including files, networks, etc
    • Immediates, the function registered via setImmediate
    • Close Handlers, a callback to a close event, such as a TCP connection failure
  • Tasks are synchronized and the MicroTask queue is emptied after each phase
    • Priority to emptynext tick queue, that is, throughprocess.nextTickRegistered functions
    • Then clear the Other Queue, common as Promise
  • The difference is that Node empties the queue for the current phase and executes all tasks

Rechallenge the Test (2)

Now that you know the implementation, go back to Test (2) :

// the code is abbreviated to // 1setTimeout(() => { // ... / / 2})setTimeout(() => {
	// ...
})
Copy the code

It can be seen that because the two setTimeouts have the same delay, they are merged into the same Expired timers queue and executed together. Therefore, as long as the delay of the second setTimeout is changed to more than 2ms (1ms is invalid, see above), both setTimeout will not expire at the same time and the consistency of output results can be guaranteed.

So if I change one of the setTimeout sections to setImmediate, does that guarantee the output sequence?

The answer is no. While it is possible to guarantee that setTimeout and setImmediate’s callbacks will not be executed together, it is not possible to guarantee the order in which setTimeout and setImmediate’s callbacks are executed.

Under Node, for the simplest example, the output of the following code is not guaranteed:

setTimeout(() => {
	console.log(0)	
})

setImmediate(() => {
	console.log(1)
})

// or
setImmediate(() => {
	console.log(0)
})

setTimeout(() => {
	console.log(1)	
})
Copy the code

The key is when setTimeout expires, and only an expiring setTimeout is guaranteed to perform before setImmediate.

For example (2), the consistency of output is basically guaranteed, but it is strongly not recommended:

/ / to use firstsetThe Timeout registeredsetTimeout(() => { // ... }) // A series of micro tasks are executed, guaranteedsetNew Promise(resolve => {//... }) process.nextTick(() => { // ... }) // reusesetImmediate Registers, and executes after "almost" ensuressetImmediate(() => {
	// ...
})
Copy the code

Or another way to keep order:

const fs = require('fs')

fs.readFile('/path/to/file', () = > {setTimeout(() => {
        console.log('timeout')})setImmediate(() => {
        console.log('immediate')})})Copy the code

So why does such code ensure that setImmediate’s callback takes precedence over setTimeout’s?

Because the current Node Event Loop is in the I/O queue phase after both callbacks are registered, and the next phase is immediates Queue, it is guaranteed that the setImmediate callback will be executed even after setTimeout has expired.

PART 3: Application

Since I have just learned Event Loop, I can think of few application scenarios, whether relying on specifications or implementation. So what can we use Event Loop for?

Check the Bug

Normally, we wouldn’t encounter very complex queue scenarios. But in case we run into something, such as an uncertain order of execution, we can quickly locate the problem.

The interview

So when is there a complex queue scenario? For example, the interview, no doubt there will be this weird test, so it will be easy to cope with ~

Executive priority

In all seriousness, microTask takes precedence over Task execution in terms of specifications. If there is logic that needs to be executed first, the microTask queue will be executed before the task. This feature can be used to design the task scheduling mechanism in the framework.

If you look at the Node implementation, microTask execution can even block I/O if the timing is right, which is a double-edged sword.

In summary, higher-priority code can be executed using the Promise/ process.nexttick registry.

Execution efficiency

From the implementation of Node, setTimeout timer type API needs to create timer objects and iterate, and the task processing needs to operate the small root heap, with time complexity of O(log(n)). In contrast, Process. nextTick and setImmediate have O(1) complexity and are more efficient.

If performance is required, use Process. nextTick and setImmediate preferentially.

other

Welcome to add ~

reference

  • This time, thoroughly understand the JavaScript execution mechanism
  • Tasks, microtasks, queues and schedules
  • Event Loop and the Big Picture
  • Timers, Immediates and Process.nextTick
  • What you should know to really understand the Node.js Event Loop
  • Node asynchronizes those things
  • libuv design

If you are interested in the team, you can follow the column or send your resume to ‘tao.qit####alibaba-inc.com’.replace(‘####’, ‘@’)

Original address: github.com/ProtoTeam/b…