一、首先来执行以下代码:代码1
function foo() {
bar()
console.log('foo')
}
function bar() {
baz()
console.log('bar')
}
function baz() {
setTimeout(() => {
console.log('setTimeout')
}, 2000)
console.log('baz')
}
foo()
输出为
baz bar foo setTimeout
那他的执行顺序是怎样的呢?
先看一下ES5的事件循环
对照上图我们分析一下代码1的执行过程:
- 遇到foo函数被调用,将foo函数压入执行栈。
- 执行foo函数,发现foo函数体中调用了bar函数,则将bar函数压入执行栈。
- 执行bar函数,发现bar函数体中调用了baz函数,又将baz函数压入执行栈。
- 执行baz函数,函数体中第一句为setTimeout函数,js线程将其交给Web APIs(Web APIs会将其按照一定的规则加入任务队列),自己继续往下执行
console.log('baz')
,在控制台打印:baz,然后baz函数执行完毕弹出执行栈。 - 此时的栈顶为bar函数,bar函数体中的baz()语句已经执行完,接着执行
console.log('bar')
,在控制台打印:bar,然后bar函数执行完毕弹出执行栈。 - 此时的栈顶为foo函数,foo函数体中的bar()语句已经执行完,接着执行
console.log('foo')
,在控制台打印:foo,然后foo函数执行完毕弹出执行栈。 - 至此,执行栈为空,这一轮执行完毕。
- 接下来就回去任务队列中查看是否有待执行的任务,发现有,则执行
console.log('setTimeout')
过程如下:
二、ES6中新增的promise
ES6中新增的promise,ES6+标准中的任务队列也新增了一种,变成了如下两种:
- 宏任务队列(大家称之为macrotask queue,即callback queue):按HTML标准严格来说,其实没有macrotask queue这种说法,它也就是ES5中的事件队列,该队列存放的是:DOM事件、AJAX事件、setTimeout事件等的回调。可以通过setTimeout(func)即可将func函数添加到宏任务队列中。
- 微任务队列(microtask queue):存放的是Promise事件、nextTick事件(Node.js)等。有一个特殊的函数queueMicrotask(func)可以将func函数添加到微任务队列中。
那么,现在的事件循环模型就变成了如下的样子:
- JS线程负责处理JS代码,当遇到一些异步操作的时候,则将这些异步事件移交给Web APIs 处理,自己则继续往下执行。
- Web APIs线程将接收到的事件按照一定规则添加到任务队列中,宏事件(DOM事件、Ajax事件、setTimeout事件等)添加到宏任务队列中,微事件(Promise、nextTick)添加到微事件队列中。
- JS线程处理完当前的所有任务以后(执行栈为空),它会先去微任务队列获取事件,并将微任务队列中的所有事件一件件执行完毕,直到微任务队列为空后再去宏任务队列中取出一个事件执行(每次取完一个宏任务队列中的事件执行完毕后,都先检查微任务队列)。
- 然后不断循环第3步。
接下来执行如下代码:代码2
function foo() {
console.log('foo')
}
console.log('global start')
setTimeout(() => {
console.log('setTimeout:')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('promise then')
})
foo()
console.log('global end')
输出为:
global start
promise
foo
global end
promise then
setTimeout
代码执行的解释:
- 执行
console.log('global start')
语句,打印出:global start。 - 继续往下执行,遇到setTimeout,JS执行栈将其移交给Web API处理。 延迟0秒后,Web API将setTimeout事件添加到宏任务队列。
- JS线程转交setTimeout事件后自己则继续往下执行,遇到new Promise(…),执行之,Promise参数中的匿名函数同步执行,执行
console.log('promise')
打印出:promise。在执行resolve()之后Promise状态变为resolved,再继续执行then(…),遇到then则将其提交给Web API处理,Web API将其添加到微任务队列。 - 执行栈在转交完Promise事件后,继续往下执行,到达语句foo(),执行foo函数,打印出foo。
- 执行栈继续执行,到达语句
console.log('global end')
,执行后打印出:global end。至此,本轮事件循环已结束,执行栈为空。 - 事件循环机制首先查看微任务队列是否为空,发现有一个Promise事件待执行,则将其压入执行栈,执行then中的代码,执行
console.log('promise then')
,打印出:promise then。至此,新的一轮事件循环(Promise事件)已结束,执行栈为空。 - 执行栈变空后又先查看微任务队列,发现微任务队列已为空,然后再查看宏任务队列,发现有setTimeout事件待处理,则将setTimeout中的匿名函数压入执行栈中执行,执行
console.log('setTimeout')
语句,打印出:setTimeout: 0s。至此,新的一轮事件循环(setTimeout事件)已结束,执行栈为空。 - 执行栈变空后又先查看微任务队列,发现微任务队列已为空,然后再查看宏任务队列,发现宏任务队列也为空,那么执行栈进入等待事件状态。
再来看下执行过程:
三、事件循环中的async/await
(不了解的人建议先去补习一下:async)
function foo() {
console.log('foo')
}
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('global start')
async1()
foo()
console.log('global end')
输出结果为:
global start
async1 start
async2
foo
global end
async1 end
我们再来逐条解析一下代码的执行过程吧:
-
首先执行console.log(‘global start’),打印出:global start。
-
执行async1(),进入到async1函数体内,执行console.log(‘async1 start’),打印出:async1 start。接着执行await async2(),这里await关键字的作用就是await下面的代码只有当await后面的promise返回结果后才可以执行(此时,微任务队列有一事件,其实就是Promise事件),而await async2()语句就像执行普通函数一样执行async2(),进入到async2函数体中;执行console.log(‘async2’),打印出:async2。async2函数执行结束弹出执行栈。
-
因为await关键字之后的语句已经被暂停,那么async1函数执行结束,弹出执行栈。JS主线程继续向下执行,执行foo()函数打印出:foo。
-
执行console.log(‘global end’),打印出:global end。该语句之后再无其他需执行的代码,执行栈为空,则本轮事件执行结束。
-
此时,事件循环机制开始工作:同理,先查看微任务队列,执行完所有已存在的微任务事件后再去查看宏任务队列。目前微任务队列中的事件即为async1函数中await async2()语句,async2函数执行完毕后,promise状态变为settled,之后的代码就可以继续执行了(可以这么理解:用一个匿名函数包裹await语句之后的代码作为一个微任务事件),执行console.log(‘async1 end’)语句,打印出:async1 end。执行栈又为空,本轮事件也执行结束。
-
事件循环机制再查看微任务队列,发现为空,再去查看宏任务队列,发现也为空,则进入等待事件状态。
文本转载自:https://blog.csdn.net/cc18868876837/article/details/97107219
原博主写的超详细,我在这里简单写了一下,方便以后自己复习,如果上面的看完还不太理解,建议可以去看下原文。