ES5中事件循环
执行以下代码:
function a() {
b()
console.log('a')
}
function b() {
c()
console.log('b')
}
function c() {
setTimeout(() => {
console.log('setTimeout')
}, 2000)
console.log('c')
}
a()
//输出为:c b a setTimeout
那他的执行顺序是怎样的呢?先看一下ES5的事件循环流程图:
对照上图分析代码的执行过程:
- 遇到a函数被调用,将a函数压入执行栈。
- 执行a函数,发现a函数体中调用了b函数,则将b函数压入执行栈。
- 执行b函数,发现b函数体中调用了c函数,又将c函数压入执行栈。
- 执行c函数,函数体中第一句为setTimeout函数,js线程将其交给Web APIs(Web APIs会将其按照一定的规则加入任务队列),自己继续往下执行console.log(‘c’),在控制台打印:c,然后c函数执行完毕弹出执行栈。
- 此时的栈顶为b函数,b函数体中的c()语句已经执行完,接着执行console.log(‘b’),在控制台打印:b,然后b函数执行完毕弹出执行栈。
- 此时的栈顶为a函数,a函数体中的b()语句已经执行完,接着执行console.log(‘a’),在控制台打印:a,然后a函数执行完毕弹出执行栈。
- 至此,执行栈为空,这一轮执行完毕。
- 接下来就回去任务队列中查看是否有待执行的任务,发现有,则执行console.log(‘setTimeout’)
动画演示执行过程:
动画中 foo()、bar()、baz() 分别表示上文中的a()、b()、c()
ES6事件循环
由于ES6中新增的promise,所以任务队列变成了如下两种:
- 宏任务队列(大家称之为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步。
执行以下代码:
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事件)已结束,执行栈为空。
- 执行栈变空后又先查看微任务队列,发现微任务队列已为空,然后再查看宏任务队列,发现宏任务队列也为空,那么执行栈进入等待事件状态。
动画演示执行过程:
ES7新增async/await
执行以下代码:
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。执行栈又为空,本轮事件也执行结束。
- 事件循环机制再查看微任务队列,发现为空,再去查看宏任务队列,发现也为空,则进入等待事件状态。