Let’s start with a simple question

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

Promise.resolve().then(() = >{
   console.log(2)})console.log(0) 
Copy the code

This question is a basic survey of THE JS event loop, including the understanding of macro tasks and micro tasks. The execution result is:

0
2
1
Copy the code

If you don’t know much about these issues, then you need to know what JS event loops are.

Event loop

We know that JS is single-threaded and non-blocking

  • By single-threaded, I don’t mean that the entire JS event loop is single-threaded (in fact, there are multiple threads involved in the event loop), but that the JS engine execution thread is single-threaded (which in Chrome is V8’s thread), as shown on the left. This thread is only responsible for parsing THE JS code and processing the code blocks in the JS execution stack.
  • Non-blocking means that the JS engine execution thread is not blocked by I/O (network requests, file reads, etc.)

This is thanks to the Event Loop mechanism used by JS, known as Event Loop.

SetTimeout function

How does setTimeout, the most commonly used asynchronous function, work in an Event Loop

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

console.log(0)
Copy the code
  • The “JS engine thread” runs the code above in the JS execution stack, calling the browser’s Web API: setTimeout function, as shown in the figure “1”.

As can be seen from the above figure, setTimeout and other asynchronous interfaces are not actually in the JS engine, but in the Web browser (V8 in the figure is the JS engine of Chrome, Safari and Firefox are their respective engines, refer to mainstream Browser Kernel and JS Engine).

  • The “JS engine thread” then executes console.log(0), and the console outputs 0
  • The “timer thread” in the browser takes over the scheduled task. When the scheduled task times out, the callback function console.log(1) is inserted into the macro task queue for scheduling, as shown in figure 2.
  • “Event Loop thread” checks whether the JS execution stack is empty, and if so, pushes the tasks in the macro task queue into the JS execution stack, as shown in figure 8.
  • The “JS engine thread” processes console.log(1) code in the execution stack, and outputs 1 in the console

As you can see from this flow, no matter how short the timeout is set, the setTimeout callback must wait until the next event loop to be processed. It is easy to understand why 0 is printed before 1

Promise. Then function

Promise.resolve().then(() = >{
   console.log(2)})console.log(0)
Copy the code
  • The “JS engine thread” runs the JS execution stack to execute the code above. Since Promise first completes the resolve function, it immediately executes the promise.then method, which actually creates a microtask and pushes console.log(2) into the microtask queue. As shown in Figure 5.
  • The “JS engine thread” then executes console.log(0), and the console outputs 0
  • “Event Loop thread” checks whether the JS execution stack is empty. If so, the tasks in the microtask queue will be inserted into the JS execution stack, as shown in figure 8.
  • The “JS engine thread” processes console.log(2) code in the execution stack, and outputs 2 in the console

Macro and micro tasks

Going back to the problem at the beginning of the article, it’s just a mixture of macro and micro tasks on top of the above scenario

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

Promise.resolve().then(() = >{
   console.log(2)})console.log(0) 
Copy the code
  • The setTimeout and promise.then callbacks are stuffed into the macro task queue and then the microtask queue
  • The JS stack is cleared after console.log(0) is executed
  • The Event Loop thread detects that the JS execution stack is empty. Since the microtask queue has a higher priority than the macro task queue, the microtask is placed in the JS execution stack for processing first
  • After the microtask is processed, the tasks in the macro task queue are processed
  • So the output is 0, 2, 1

Task classification

Tasks in the JS event loop fall into two broad categories

  • A macro task is a task
  • A microTask is also called a job

For the browser environment and Node environment, the interface provided will be slightly different

Macro task interface

interface The browser Node
I/O operations
setTimeout
setInterval
setImmediate
requestAnimationFrame

Microtask interface

interface The browser Node
process.nextTick
promise.then/catch/finally
MutationObserver

Task priority

  • On the whole, microtasks take precedence over macro tasks. To be precise, microtasks are processed before the end of this event loop, and macro tasks are processed at the beginning of the next event loop.
  • In the Node environment, the microtasks generated by process.nextTick have the highest priority and will be inserted into the front of the microtask queue for priority processing. SetImmediate, on the other hand, generates the macro task with the highest priority and executes before all macro tasks

Promise

Promise is an asynchronous solution that is more intuitive and reasonable to use than the previous callback method.

  • A Promise object has three states: Pending, fulfilled, and Rejected. This is a pity.
  • When we create a Promise object using the New Promise method, the object is in a pending state.
  • For the promise.then method, there are several points:
    • First, in order to guarantee the chained invocation of a Promise, the Promise specification defines that the then method must return a Promise object
    • Second, the success and Times callback functions are wrapped in microtasks in the THEN method
    • Finally, the state of the current Promise object determines what to do:
      • In pending state, the THEN method stores wrapped success and failure callback functions in separate queues (call them success callback queues and failure callback queues), which are handled by the resolve and Reject methods
      • In the depressing state, the THEN method will directly call the wrapped success callback, that is, put the success callback function into the micro-task queue and wait for the Event Loop scheduling
      • Similarly, in the Rejected state, the then method will directly call the wrapped failure callback, that is, put the failure callback function into the micro-task queue and wait for the Event Loop scheduling
  • For Promise’s resolve and Reject methods, resolve, for example, will execute all tasks in the successful callback queue at one time. Since tasks in the successful callback queue are wrapped by microtasks, the callback functions will be successively put into the microtask queue to wait for Event Loop scheduling

Consider the following example:

let promise = new Promise((resolve, reject) = > {
  console.log(1)
  setTimeout(() = > {
    resolve(2)},1000)
})

promise.then(data= > {
  console.log(data)
})

console.log(0)
Copy the code
  • Execute the new Promise constructor
    • Console Printing 1
    • Create a scheduled task that executes the resolve method after timeout
  • Execute promise.then, since promise is still pending, so the console.log(data) function wrapped in microtask is inserted into the success callback queue, pseudo-code is as follows:
// This is pseudocode. The actual packaging is more complex than this
const successCb = () = > {
  queueMicroTask((data) = > {
  	console.log(data)
	})
}

successCbQueue.push(successCb)
Copy the code
  • Execute console output 0
  • When the timer expires after 1 second, place the resolve(2) callback on the macro task queue
  • The Event Loop schedules the macro task and the JS thread executes resolve(2), which causes the state of the Promise object to change and begins processing tasks in the success callback queue
  • To process the callback task queue, place (data) => console.log(data) in the microtask queue
  • If (data) => console.log(data), resolve(2) will pass in data, so the console will print 2
  • The printing sequence is 1, 0, and 2

Chain calls

The basic execution order of promises should be clear from the example above, but let’s look at the more complex issue of how the code works when a promise.then chain call is made.

The question comes from this article: “Starting with a Promise Interview question that Keeps Me Up at night, Breaking down the Promise Implementation details.”

Promise.resolve().then(() = > {
    console.log(0);
    return Promise.resolve(4);
}).then((res) = > {
    console.log(res)
})

Promise.resolve().then(() = > {
    console.log(1);
}).then(() = > {
    console.log(2);
}).then(() = > {
    console.log(3);
}).then(() = > {
    console.log(5);
})
Copy the code

Before we look at this complex problem, let’s look at two simple examples of promise.resolve and promise.then

Promise.resolve()

Promise. Resolve is a static method that essentially creates a Promise object that immediately resolves. The following two pieces of code are equivalent

Promise.resolve(0)

/ / is equivalent to
new Promise((resolve, reject) = > {
  resolve(0)})Copy the code

So,

Promise.resolve(0).then(data= > console.log(data))
Copy the code

How to execute this code:

  • This is a big pity. Create a Promise object and immediately change its state to depressing
  • The promise. Then method will be implemented. As the promise state at this time is fulfilled, the subsequent success callback function will be directly stuffed into the micro-task queue for scheduling

Promise.then()

As we know, the then method returns a new Promise object, providing the nature of a chain call. Consider this example:

Promise.resolve(0).then(data= > {
  console.log(data)
  return 1
}).then(data= > {
  console.log(data)
})
Copy the code

We omit the details and simply describe the process of this cycle of events

  • The first THEN creates a microtask
  • For the first microtask, the console prints 0 and returns 1, which is used as an input to the success callback function for the next THEN
  • The second then creates a new microtask
  • Execute the second microtask, console output 1

As you can see, when THEN returns a normal object, it simply passes that object as an input parameter to the next THEN call. But what happens when then returns a Promise object?

So let’s look at this code

Promise.resolve(0).then(data= > {
  console.log(data)
  return Promise.resolve(1)
}).then(data= > {
  console.log(data)
})
Copy the code

In the first THEN, we return a Promise object in a depressing state. If we still follow the processing logic of ordinary objects and directly hand the Promise object as an input to the next THEN function, the output of the code above will become

0
Promise {<fulfilled>: 1}
Copy the code

Obviously, this is not what we expected. Resolve (1) is executed and the result of resolve(in this case, 1) is passed to the next THEN. So, in practice, when a Promise object is returned in the THEN method, the Promise object is executed before any subsequent THEN methods are processed. To understand this mechanism, we can simply think of two THEN functions as inserting a THEN function

// Simplify the understanding of the code execution order, the actual processing process will be one more microtask queue, which will be discussed later
Promise.resolve(0).then(data= > {
  console.log(data)
}).then(data= > {
	return 1
}).then(data= > {
  console.log(data)
})
Copy the code

The result of executing the code in this example is

0
1
Copy the code

In fact, the mechanism for handling the Promise objects returned in the THEN method is more complex than the simplified version above.

  • V8 first created a NewPromiseResolveThenableJobTask types of tasks, and then put the task detail task queue waiting to be processed in the micro tasks (+ 1)
  • When the task to be scheduled, when handling NewPromiseResolveThenableJobTask, actual is called Promise. Then method, and created a small task at this time (micro task + 1)
  • So, it actually takes two micro-tasks to shift this Promise object into a depressing state

Detailed source code analysis please refer to this article: juejin.cn/post/695345…

Comprehensive analysis of

With that in mind, let’s get back to the complicated question

// PromiseA
Promise.resolve().then(() = > {
    console.log(0);
    return Promise.resolve(4);
}).then((res) = > {
    console.log(res)
})

// PromiseB
Promise.resolve().then(() = > {
    console.log(1);
}).then(() = > {
    console.log(2);
}).then(() = > {
    console.log(3);
}).then(() = > {
    console.log(5);
})
Copy the code

For illustrative purposes, an equivalent rewrite of the above code is made

// 1. This is a big Promise
const PromiseA = Promise.resolve()

// Due to the depressing state, the successful callback in the then function will be directly placed into the micro-task to wait for processing, and the state of PromiseA1 is pending
const PromiseA1 = PromiseA.then(() = > {
    console.log(0);
    return Promise.resolve(4);
})

// 3. Since the status of PromiseA1 is pending, then functions will be put into the success callback queue and wait for processing when the status of PromiseA1 becomes DEPRESSING
const PromiseA2 = PromiseA1.then((res) = > {
    console.log(res)
})

// this is a big Promise
const PromiseB = Promise.resolve()

// This is a big pity. The successful callback in the then function will be directly placed into the micro-task to be processed, and the status of PromiseB1 is pending
const PromiseB1 = PromiseB.then(() = > {
    console.log(1);
})

// 6. Since the status of PromiseB1 is pending, then functions will be put into the success callback queue for processing when the status of PromiseB1 becomes depressing
const PromiseB2 = PromiseB1.then(() = > {
    console.log(2);
})

// 7. Add it to the success callback queue of PromiseB2
const PromiseB3 = PromiseB2.then(() = > {
    console.log(3);
})

// 8. Add it to the success callback queue of PromiseB3
const PromiseB4 = PromiseB3.then(() = > {
    console.log(5);
})
Copy the code

The above code executes sequentially and synchronously first, and you can see that there are only two microtask tasks created in steps 2 and 5 in the microtask queue

--------------------
console.log(0)
return Promise.resolve(4);
--------------------
console.log(1)
--------------------
Copy the code

The two tasks are executed in sequence, resulting in

  • The console outputs 0, 1 in turn
  • As the successful callback of the PromiseA returns a Promise object, according to our previous discussion, this operation will take two rounds of micro-tasks to change the state of the PromiseA1 to a DEPRESSING, so the implementation of PromiseA2 will take two rounds
  • The PromiseB1 state becomes a big pity, and the successful callback of the then method will continue to be processed in the micro-task

Status of the second microtask queue

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- wait for PromiseA1 state changes to fulfilled -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - the console. The log (2) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --Copy the code

Similarly, the status of the third round of microtask queue

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to wait PromiseA1 state changes to fulfilled -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - the console. The log (3) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --Copy the code

In the fourth round, the state of PromiseA1 will finally change to depressing, and the successful callback of the then method will be put into the micro-task

--------------------
console.log(res)  // res = 4
--------------------
console.log(3)
--------------------
Copy the code

The fifth round

--------------------
console.log(5)
--------------------
Copy the code

So, the console output is

0
1
2
3
4
5
Copy the code

async/await

The emergence of async/await syntactic sugar makes JS code writing and reading more intuitive, allowing us to use synchronous thinking to write asynchronous logic.

As opposed to the equivalent of Promise

Async /await is actually a syntactic sugar implemented by the Genorator mechanism. In order to understand the execution order, these two syntactic sugars can be simplified as:

  • Async: Async modified function that automatically returns a Promise object, such as the function funcB
async function funcB() {
	console.log("I'm function B")}/ / is equivalent to
function funcB() {
  return new Promise((resolve, reject) = > {
    console.log("I'm function B")
    resolve()
  })
}

// As you can see, funcB synchronously executes the console output I'm Function B and returns a fulfilled Promise object
Copy the code
  • Await: Within a function, the code appearing after the await statement (without the await line) is wrapped in a then function, as shown in the following example:
async function funcB() {
	console.log("I'm function B")}async function funcA() {
  console.log("funcA start")
  await funcB()
  console.log("funcA end")}// As funcB is actually packaged as a Promise, the above code can be simply equivalent to
function funcA() {
  console.log("funcA start")
  new Promise((resolve, reject) = > {
    console.log("I'm function B")
    resolve()
  }).then(() = > {
    // Wrap the contents of the await statement in the then function
    console.log("funcA end")})}Copy the code

Additional discussion of Async

What if async Func itself returns a Promise?

// For funcB as follows
async function funcB() {
	console.log("I'm function B")
  return Promise.resolve().then(() = > {
    console.log('function B end')})}// is equivalent to this extra Promise wrapper
new Promise((resolve, reject) = > {
  console.log("I'm function B")
  return Promise.resolve().then(() = > {
    console.log('function B end')
  }).then(() = > {
    resolve()
  })
})

// This is equivalent to returning the original Promise
console.log("I'm function B")
return Promise.resolve().then(() = > {
  console.log('function B end')})// The result is the second, which returns the original Promise without additional packaging
Copy the code

To verify the above statement, consider the following example

async function funcA() {
  await funcB()
  await funcC()
  console.log(4)}async function funcB() {
	console.log(1)}async function funcC() {
  console.log(2)
	return Promise.resolve().then(() = > {
		console.log(3)
  })
}

funcA()

Promise.resolve().then(() = > {
  console.log(5)
}).then(() = > {
  console.log(6)
}).then(() = > {
  console.log(7)
}).then(() = > {
  console.log(8)})// 1, 2, 5, 3, 6, 7, 4, 8
Copy the code

Note that “4” is printed after “7”. You can see why this is the case if you read the following equivalent, right

The above code is equivalent to

//
new Promise((resolve,reject) = > {
  console.log(1)
  resolve()
}).then(() = > {
  console.log(2)
  return Promise.resolve().then(() = > {
    console.log(3)
  })
}).then(() = > {
  console.log(4)})Promise.resolve().then(() = > {
  console.log(5)
}).then(() = > {
  console.log(6)
}).then(() = > {
  console.log(7)
}).then(() = > {
  console.log(8)})// 1, 2, 5, 3, 6, 7, 4, 8
Copy the code

Note: This refers to one of the points we discussed earlier, that returning a Promise in then requires two microtasks to complete the state transition, so the “4” output comes after “7”. This also explains why “4” is printed after “7” in async/await notation.

With that in mind, let’s take a look at the following code execution order

async function funcA() {
  console.log("funcA start")
  await funcB()
  console.log("funcA end")}async function funcB() {
  console.log("funcB start")
	return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      console.log("funcB end")
      resolve()
    }, 1000)})}console.log("script start")
funcA()
console.log("script end")
Copy the code

The above code is equivalent to

console.log("script start")
console.log("funcA start")
console.log("funcB start")
new Promise((resolve, reject) = > {
  setTimeout(() = > {
    console.log("funcB end")
    resolve()
  }, 1000)
}).then(() = > {
  console.log("funcA end")})console.log("script end")
Copy the code
  • First, the console prints “Script start”, “funcA start”, “funcB Start”.
  • Run setTimeout to wait for a timeout
  • Attach console.log(“funcA end”) to the Promise’s success team
  • Output “script end”
  • The timer timed out, and the macro task was inserted to wait for scheduling
  • The Event Loop thread schedules the macro task, printing “funcB end” and changing the Promise state to fulfiiled to execute the task in the success queue (console.log(“funcA end”) wrapped with microtasks).
  • Console. log(“funcA end”) is placed on the microtask queue to wait for scheduling
  • The Event Loop thread dispatches the microtask, printing “funcA End”
  • Therefore, the output is “script start”, “funcA start”, “funcB start”, “script end”, “funcB end”, “funcA end”

One more twist, in funcB, does not return a Promise containing the timer

async function funcA() {
  console.log("funcA start")
  await funcB()
  console.log("funcA end")}// remove return
async function funcB() {
  console.log("funcB start")
  new Promise((resolve, reject) = > {
    setTimeout(() = > {
      console.log("funcB end")
      resolve()
    }, 1000)})}console.log("script start")
funcA()
console.log("script end")
Copy the code

The output becomes “script start”, “funcA start”, “funcB start”, “script end”, “funcA end”, “funcB end”. It does not need to wait for the timer’s Promise execution to end, so “funcA End” is executed before “funcB End”. And just to make it more intuitive, let’s do the same thing

console.log("script start")
console.log("funcA start")

// Since funcB itself does not return a Promise, we need to wrap an additional Promise layer around it
new Promise((resolve, reject) = > {
  console.log("funcB start")
  new Promise((resolveInner, rejectInner) = > {
    setTimeout(() = > {
      console.log("funcB end")
      resolveInner()
    }, 1000)
  })
  resolve()
}).then(() = > {
  console.log("funcA end")})console.log("script end")
Copy the code

The above equivalent notation is only used to intuitively understand the execution order of await/async, and is not equivalent to the implementation principle.

summary

  • The key to understanding Event Loops,
    • First of all, we know the main modules involved in Event Loop operation: JS engine execution thread, JS execution stack, Web/Nodejs Api, macro task, micro task, Event Loop scheduling thread.
    • Second, to understand the relationship between them, V8 only contains the JS engine execution thread, THE JS execution stack, the Promise interface, and the rest is provided by the browser or node environment
  • JS code execution order, just need to clarify which are macro tasks, which are micro tasks. Microtasks are executed at the end of this event loop, and macro tasks are executed at the beginning of the next event loop, so those that appear to be microtasks are executed before macro tasks.
  • In microtasks, process.nextTick has a higher priority than other microtasks
  • Microtasks have no recursion limit and macro tasks have stack overflow limit. So microtask use should be careful not to abuse recursion, otherwise it will lead to an infinite loop.
  • SetTimeout specifies exactly how long it will take to execute the callback. For example, setTimeout(() => XXX, 1000) refers to the XXX operation after at least 1000ms. Because setTimeout’s callback is in the macro task, if a large number of synchronous operations or micro-tasks are performed in this event loop, the macro task will be delayed.
  • Promise execution requires understanding the behavior of the resolve and THEN functions
    • Resolve changes the state of the Promise object and executes all success callbacks for that Promise object.
    • Then wraps the success/failure callback function based on the state of the Promise object, and the callback function is advanced into the microtask queue when executed
    • Then itself returns a new Promise object
    • If a Promise object is returned in the callback function of THEN, two micro-tasks are required to change the state of the Promise object returned by THEN to the depressing state
  • Async /await can be written with Promise equivalently. Async wraps a function as a Promise object, and await is to execute subsequent code in then.

The following references can be read in depth

reference

MDN: Concurrency model and time loop

JS execution stack diagram

Queues and schedules (emphasis recommended, visually display the execution process of JS execution stack, microtasks, macro Tasks)

Philip Roberts talks about EventLoop at the JS 2014 Conf

Event Loop Visualization (a visualization tool by Philip Roberts, with video)