The article was first published on my blog

preface

I’ve been interviewing a lot of companies recently, and this question is almost a must-ask. I always thought I knew about it before, but when I was asked for the first time, I didn’t know where to start. There were a lot of knowledge points involved. So I took the time to sort it out. Not just because of the interview, but because understanding the JavaScript event loop will answer the usual questions.

That’s what the interviewer will ask you to do, ask you to print out the results. What is the difference between a macro task and a micro task, and why there are two kinds of tasks?

This article refers to a lot of articles, at the same time add their own understanding, if there is a problem I hope you point out.

Event loop

  1. JavaScript is single-threaded and non-blocking
  2. Browser event loop
    • Execution stack and event queue
    • Macro and micro tasks
  3. Event loop in node environment
    • How it differs from the browser environment
    • Event cycle model
    • Macro and micro tasks
  4. Classic topic analysis

1. JavaScript is single-threaded and non-blocking

Single thread:

The primary purpose of JavaScript is to interact with users and manipulate the DOM. If it is multi-threaded, there are a lot of complex issues to deal with, such as two threads operating on the DOM at the same time, one thread deleting the current DOM node, and one thread operating on the current DOM phase. Which thread will operate on? To avoid this, JS is single threaded. Even though H5 introduced the Web worker standard, it has many limitations and is controlled by the main thread, which is a child thread of the main thread.

Non-blocking: Implemented via the Event loop.

2. The browser event loop

Execution stack and event queue

To better understand the Event Loop, take a look at the following figure (as quoted from Philip Roberts’ talk “Help, I’m Stuck in an Event-Loop”)

Execution stack: Synchronizes the execution of code, adding it to the execution stack in sequence

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
}
a();
Copy the code

We can see how the above code executes by using Loupe, a visual tool that helps you understand how JavaScript call stacks/event loops/callback queues interact.

  1. Executive functiona()First in the stack
  2. a()To execute the function firstb()functionb()Into the stack
  3. Executive functionb().console.log('b')Into the stack
  4. The outputb.console.log('b')Out of the stack
  5. functionb()Execute complete, exit the stack
  6. console.log('a')Push, execute, outputaAnd out of the stack
  7. Function A completes and exits the stack.

Event queue: The execution of asynchronous code that does not wait for an asynchronous event to return a result, but suspends the event and continues to execute other tasks in the execution stack. When an asynchronous event to return the result to it on the event queue and are placed in the event queue is not immediately implement the callback, but wait for the current execution stack of all tasks are completed, the main thread idle state, the main thread to find whether there is a task in the event queue, if you have, then remove the top of the list events, and put the corresponding callback event in execution stack, The synchronization code is then executed.

Let’s add asynchronous events to the code above,

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
    setTimeout(function() {
        console.log('c');
    }, 2000)
}
a();
Copy the code

The execution process is as follows

Let’s also add click events to see what’s going on

$.on('button'.'click'.function onClick() {
    setTimeout(function timer() {
        console.log('You clicked the button! ');    
    }, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
    console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");
Copy the code

Briefly summarize with the following figure

Macro and micro tasks

Why microtasks, not just one type of task?

Page rendering events, various IO completion events, etc. are added to the task queue at any time, and are always implemented on a first-in, first-out basis. We cannot control exactly where these events are added to the task queue. But then suddenly there are high-priority tasks that need to be executed as soon as possible, so one type of task is not suitable, so microtask queues are introduced.

The different asynchronous tasks are divided into: macro and micro tasks macro tasks:

  • Script (whole code)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI interaction events

Micro tasks:

  • New Promise().then(callback)
  • MutationObserver(new html5 feature)

Operation mechanism

The result of an asynchronous task is placed in a task queue, and depending on the type of asynchronous event, the event is actually placed in the corresponding macro and microtask queues.

When the current stack is empty, the main thread checks for events on the microtask queue

  • If yes, execute the callback corresponding to the event in the queue until the microtask queue is empty, then fetch the first event from the macro task queue and add the current callback to the current pointing stack.
  • If not, fetch an event from the macro task queue and add the corresponding event back to the current stack.

The current stack processes all events in the microtask queue immediately after execution, and then fetches an event from the macro task queue. Microtasks are always executed before macro tasks in the same event loop.

In an event cycle, each operation is called tick. The task processing model of each tick is complex, but the key steps are as follows:

  • Perform a macro task (fetch from event queue if not in stack)
  • If a microtask is encountered during execution, it is added to the task queue of the microtask
  • Execute all microtasks in the current microtask queue immediately after the macro task is executed (in sequence)
  • When the macro task completes, the render is checked, and the GUI thread takes over
  • After rendering, the JS thread takes over and starts the next macro task (fetched from the event queue)

A brief summary of the execution sequence: execute the macro task, and then execute the micro-task generated by the macro task. If a new micro-task is generated during the execution of the micro-task, continue to execute the micro-task. After completing the micro-task, return to the macro task for the next cycle.

Deep understanding of the JS event loop mechanism (browser) this article has a special image of the animation, you can see the understanding.

console.log('start')

setTimeout(function() {
  console.log('setTimeout')},0)

Promise.resolve().then(function() {
  console.log('promise1')
}).then(function() {
  console.log('promise2')})console.log('end')
Copy the code

  1. Global code is pushed into execution stack execution, outputstart
  2. The setTimeout is pressed into the MacroTask queue, and the promise.then callback is put into the microTask queue and executedconsole.log('end')And the outputend
  3. The code in the call stack completes execution (the global code is a macro task), and then the code in the microtask queue executes, performing the Promise callback, and outputspromise1The promise callback function will return undefined by default. The promise state becomes a pity, which will trigger the next then callback and continue to be pressed into the microtask queue. At this time, a new microtask will be generated and the current microtask queue will be completed. The second promise.then callback is executed and outputpromise2
  4. At this point, the MicroTask queue is empty and UI rendering (if any) is performed, followed by the next event loop, which executes the setTimeout callback and outputssetTimeout

The final result is as follows

  • start
  • end
  • promise1
  • promise2
  • setTimeout

Event loop in node environment

How it differs from the browser environment

Displays roughly the same state as the browser. The difference is that Node has its own model. The implementation of event loops in Node relies on the Libuv engine. The Node event loop has several phases.

If node10 or earlier, microTask will execute between phases of the event cycle, meaning that tasks in the MicroTask queue will be executed after each phase is completed.

With node 11, the Event Loop mechanism has changed. SetTimeout,setInterval, and setImmediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate The code in the following example is parsed to the latest.

Event cycle model

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ I/O callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ connections ─ ─ ─ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ├ ──┤ close callbacks ────── our r companyCopy the code

Detailed explanation of each stage of the event cycle

The sequence of event loops in node

— poll — check — Close callback — Timer check — I/O callbacks — POLL — Check — Close callback — Timer — I/O callbacks — POLL — Check — Check — CLOSE callback — Timer — I/O callbacks — Idle, prepare…

The general functions of these stages are as follows:

  • Timers: This stage performs callbacks in timer queues such as setTimeout() and setInterval().
  • I/O event callback phase (I/O Callbacks): This phase performs almost all callbacks. But not close events, timers, and setImmediate() callbacks.
  • Idle: This stage is for internal use only and can be ignored
  • Poll phase (poll): Waiting for new I/O events. Node blocks here in some special cases.
  • Check phase: This is the phase where the setImmediate() callback executes.
  • Close callbacks: for example socket.on(‘close’,…) The callback to this close event

Poll: This phase is polling time, waiting for I/O events that have not yet returned, such as server responses, user mouse movements, and so on. This phase will take a long time. If there are no other asynchronous tasks to be processed (such as an expired timer), it stays in this phase, waiting for the I/O request to return the result. Check: This phase executes the setImmediate() callback function.

Close: This phase executes the callback function that closes the request, such as socket.on(‘close’,…). .

Timer phase: This is the timer phase, which handles callbacks to setTimeout() and setInterval(). After entering this phase, the main thread checks the current time to see if the timer conditions are met. If so, execute the callback, otherwise leave the phase.

I/O callback phase: This phase executes everything except the following callback functions:

  • Callbacks to setTimeout() and setInterval()
  • SetImmediate () ‘s callback function
  • The callback function used to close the request, such as socket.on(‘close’,…)

Macro and micro tasks

Macro task:

  • setImmediate
  • setTimeout
  • setInterval
  • Script (whole code)
  • I/O operations.

Micro tasks:

  • process.nextTick
  • New Promise().then(callback)

Promise.nextTick, setTimeout, setImmediate Use scenarios and differences

Promise.nexttick process. NextTick is a task queue independent of eventLoop. After each eventLoop phase is complete, the nextTick queue is checked and, if there are tasks in it, those tasks take precedence over microtasks. Is the fastest of all asynchronous tasks.

SetTimeout: The setTimeout() method defines a callback and expects it to be executed the first time after the interval we specify.

SetImmediate: The setImmediate() method does not perform a callback until a certain stage, after the poll stage.

Classic topic analysis

What does the following code output

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
Copy the code

The macro task (the current code block counts as a macro task) is executed, then the microtask generated by the current macro task is executed, and then the macro task continues

  1. Execute code from top to bottom, execute synchronous code first, outputscript start
  2. When setTimeout is encountered, put the setTimeout code into the macro task queue
  3. Execute async1() to printasync1 start, then async2(), outputasync2, put async2() after the codeconsole.log('async1 end')Put it in the microtask queue
  4. And then we go down and printpromise1, put.then() in the microtask queue; Note that Promise itself is a synchronous immediate execution function, and. Then is an asynchronous execution function
  5. And then we go down and printscript end. The synchronized code (which is also the macro task) completes execution, and the code you just put into the microtask begins
  6. Execute the code in the microtask in turn, and output it in turnasync1 end,promise2After the code execution in the micro task is completed, the code in the macro task is executed and outputsetTimeout

The final result is as follows

  • script start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

What does the following code output

console.log('start');
setTimeout((a)= > {
    console.log('children2');
    Promise.resolve().then((a)= > {
        console.log('children3'); })},0);

new Promise(function(resolve, reject) {
    console.log('children4');
    setTimeout(function() {
        console.log('children5');
        resolve('children6')},0)
}).then((res) = > {
    console.log('children7');
    setTimeout((a)= > {
        console.log(res);
    }, 0)})Copy the code

Executing the code creates many macro tasks, and within each macro task there are microtasks

  1. Execute code from top to bottom, execute synchronous code first, outputstart
  2. When setTimeout is encountered, place the setTimeout code in the macro task queue ①
  3. And then we go down and printchildren4If setTimeout is encountered, put setTimeout code in macro task queue ②. Then will not be put in microtask queue because resolve is executed in setTimeout
  4. When the code is finished, it looks for events in the microtask queue, finds none, and executes macro task ①, the first setTimeout, outputchildren2At this point, thePromise.resolve().thenPut it in the microtask queue.
  5. When the code in macro task 1 completes, it looks for the microtask queue and printschildren3; Then the macro task ②, the second setTimeout, is executedchildren5Then is placed in the microtask queue.
  6. When the code in macro task 2 completes, it looks for the microtask queue and printschildren7When setTimeout is encountered, place it in the macro task queue. At this point, the execution of the micro task is complete, and the macro task is executed to outputchildren6;

The final result is as follows

  • start
  • children4
  • children2
  • children3
  • children5
  • children7
  • children6

What does the following code output

const p = function() {
    return new Promise((resolve, reject) = > {
        const p1 = new Promise((resolve, reject) = > {
            setTimeout((a)= > {
                resolve(1)},0)
            resolve(2)
        })
        p1.then((res) = > {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) = > {
    console.log(res);
})
console.log('end');
Copy the code
  1. Execute the code. Promise itself is a synchronous, immediate execution function, and. Then is an asynchronous execution function. When setTimeout is encountered, place it in the macro task queuep1.thenIt’s going to put it in the microtask queue, and then it’s going to go down and print it out3
  2. encounterp().thenIt’s going to put it in the microtask queue, and then it’s going to go down and print it outend
  3. After the synchronous code block execution is complete, the tasks in the microtask queue are executed, firstp1.thenAnd the output2, then executep().thenAnd the output4
  4. After the completion of the microtask, start the macro task, setTimeout,resolve(1)But at this timep1.thenThe execution is complete1No output.

The final result is as follows

  • 3
  • end
  • 2
  • 4

You can comment out resolve(2) in the code above so that 1 is printed and 3 end 4 1 is printed.

const p = function() {
    return new Promise((resolve, reject) = > {
        const p1 = new Promise((resolve, reject) = > {
            setTimeout((a)= > {
                resolve(1)},0)
        })
        p1.then((res) = > {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) = > {
    console.log(res);
})
console.log('end');
Copy the code
  • 3
  • end
  • 4
  • 1

Finally, I strongly recommend several very good videos on event loop:

  • What the heck is the event loop anyway? | Philip Roberts | JSConf EU
  • Jake Archibald: In The Loop – JSConf.Asia

reference

  • Explain the Event Loop mechanism in JavaScript
  • More on the Event Loop
  • Node timer description
  • Interview question: Tell me about the cycle of events.
  • Geek browser working principles and practices
  • Microtasks, macro tasks, and Event-loops
  • The difference between node.js event loop and js browser event loop
  • Figure out the JavaScript engine Event Loop
  • What is the difference between browser and Node Event loops?
  • An in-depth understanding of the JS event loop mechanism (Node.js)
  • Understanding browser Event Loops
  • Loupe