1.背景介绍
大家在开发写项目的时候,都会遇到同步任务和异步任务,在异步任务里面又分了宏任务和微任务,那么就需要关注代码的执行逻辑是怎么样的,否则很容易引起线上事故。
2.事件循环讲解
2.1 浏览器js异步执行的原理
首先大家都知道js是单线程的,就是每一个时刻只能做一件事情,那为什么可以同时执行异步任务呢?那是因为浏览器是多线程的,当js需要执行异步任务的时候,浏览器就会开启另外一个线程来执行异步任务,浏览器除了有js引擎线程(主线程)外,还有定时器线程和HTTP请求线程等。
2.2 浏览器中的事件循环
2.2.1 执行栈与任务队列
js在解析代码的时候,会将同步任务排在某个地方,然后按照顺序进行执行里面的函数,即执行栈。当遇到其他异步任务时,浏览器会交给其他线程进行处理,等到执行完同步代码以后,会从任务队列中取出异步任务的回调加入执行栈,加入执行栈以后,就会开始执行异步回调里面的内容了,在执行的过程中又遇到异步任务了,浏览器又会把这个异步任务交给对应的线程进行执行,如此循环,直到所有的同步任务和异步任务都执行完毕。
2.2.2 宏任务和微任务
在任务队列里面根据任务的种类不同,可以分为微任务队列和宏任务队列。在任务队列中一般优先执行微任务,微任务执行完了以后再执行宏任务。
常见的宏任务:
- setTimeout()
- setInterval()
- setImmediate()
常见的微任务:
- promise.then(), promise.catch()
- new MutaionObserver()
- process.nextTick()
- async await
console.log('测试1');
new Promise((resolve) => {
console.log('测试2')
resolve()
}).then(() => {
console.log('测试4')
})
setTimeout(() => {
console.log('测试3')
}, 0)
console.log('测试6')
// 测试1,测试2,测试6,测试4,测试3
宏任务和微任务的区别是什么呢?
- 宏任务特征:有明确的异步任务需要执行和回调;需要浏览器其他线程支持。
- 微任务特征:不需要浏览器其他线程支持,只有回调。
2.2.2 async/await执行顺序
我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
} // 后面跟着的是普通函数
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。
2.3 NodeJS中的事件循环
node中的事件循环的实现是依赖libuv引擎。
node中的也有宏任务和微任务。
宏任务大概包括:
- setTimout
- setInterval
- setImmediate
- script(整体代码)
- I/O操作等
微任务大概包括:
- process.nextTick
- new Promise().then(回调)
事件循环各阶段 在NodeJS中JS的执行,主要由以下阶段:
- timers阶段:执行所有setTimeout()和setInterval()回调
- pennding callbacks阶段:除了timers、close、setImmediate的其他大部分回调在此阶段执行。
- poll阶段:轮询等待新的链接和请求等事件,执行I/O回调等,在此阶段的任务执行完毕后,就会先检查checked阶段,如果有就执行,没有就等待新的任务,在等待新的任务的时候,发现timer的时间到了,则就进入timer阶段。
- check阶段:setImmediate()回调函数在这里执行。
- close callback阶段:一些关闭的回调函数,如:socket.on('close', ...);
上面所提到的每个阶段都会执行完对应任务队列(宏任务and微任务),只有当前阶段的所有任务队列都处理完了,才会进入到下一个阶段,这个跟浏览器这边还是有一定差异的。
setTimeout(() => {
// timers 阶段
console.log('测试1')
promise.resolve().then(() => {
console.log('测试2')
})
})
fs.readFile(_fileName, (data) => {
// poll(I/O回调)阶段
console.log(测试3)
promise.resolve().then(() => {
console.log('测试4')
})
})
// 测试1,测试2,测试3,测试4
2.3.1 nextTick,setImmediate和setTimeout
process.nextTick()的执行时机是在同步任务之后,其他所有异步任务之前,会优先执行nextTick。
setImmediate和setTimeout是各自执行在不同阶段,setImmediate是在check阶段,setTimeout是在timer阶段,执行的先后顺序:
setTimeout(() => {
console.log('测试1')
}, 0)
setImmediate(() => {
console.log('测试2')
})
// 测试1,测试2
在第一轮循环中,会把setTimeout和setImmediate都放入任务队列中,第二轮timer时机到了,首先会进入timer阶段,执行定时器的回调,输出测试1,执行完发现没有pennding callbacks阶段和poll阶段需要执行,就进入checked阶段,执行结束后输出测试2。
fs.readFile(_fileName, (data) => {
console.log('poll阶段');
setTimeout(() => {
console.log('测试1')
}, 0)
setImmediate(() => {
console.log('测试2')
})
})
// poll阶段,测试2,测试1
首先进入poll阶段,执行输出poll阶段,poll阶段的任务完成后,就开始检测是否有check阶段,发现有setImmediate,所以就进入checked阶段,输出测试2,check阶段执行完毕后,发现timer的定时时机到了, 就开始执行,输出测试1。从中可以说明setTimeout和setImmediate的执行顺序跟当前执行的阶段(在poll阶段的回调)有关。