前置背景
- 默认js是(主线程)单线程的
- 栈和队列
- 栈是后进先出
- 队列是先进先出
- shift() 删除第一个元素; pop() 删除最后一个元素
- unshift() 开头添加一个元素; push() 末尾添加一个元素
- 进程和线程的关系
浏览器事件循环
举个栗子
setTimeout(() => {
console.log(1)
Promise.resolve().then(data => {
console.log(2)
})
}, 0);//这边虽然是0,但是实际上做不到,大概有4ms
Promise.resolve().then(data => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
console.log('start');
在上述代码中,js引擎会先执行主栈代码,而setTimeout和then属于异步代码,会暂时放到异步队列中,等待主栈代码执行完成后,再去执行。
毫无疑问先输出start。
在执行主栈代码的过程,遇到[1]setTimeout,[1]setTimeout是宏任务,所以等待时间到了之后,将回调函数放入宏任务队列中。
然后遇到[8]then方法,[8]then方法是微任务,因为这边直接调用了Promise.resolve() 所有马上将回调函数放入了微任务队列。
主栈代码执行完成之后,会先去清空微任务队列,所以执行[8]then回调,所以输出3。然后又遇到了一个[10]setTimeout,等待时间到之后,会将[10]setTimeout中的回调函数同样放到宏任务队列中。
清空完微任务队列后,会去执行宏任务队列中的宏任务,但只会执行一个,之后又会去清空微任务队列。所以这边先执行第一次放进队列的[1]setTimeout,所以输出1。执行过程中又遇到了第二个[3]then微任务,所以将之放入到微任务队列中。
执行完这个[1]setTimeout宏任务的之后,去清空微任务队列,所以执行第二个的微任务,所以输出2。
最后再去执行宏任务队列中的任务,这是只剩下一个[10]setTimeout,所以输出4。
所以答案是 start,3,1,2,4。([]表示行号)
宏任务和微任务
- 宏任务是宿主环境提供的,比如浏览器
- 微任务是语言本身提供的,比如promise.then()
- 常见的宏任务:setImmediate(只有ie支持)、setTimeout、requestAnimationFrame、MessageChannel、ajax
- 常见的微任务:then、queueMicroMicrotask、MutationObserver,process.nextTick(node 环境)
宏任务和微任务都是在主栈中执行,默认先执行主栈中的代码,执行后清空微任务,之后微任务执行完毕,去第一个宏任务到主栈中执行(如果有微任务再次清空微任务),再去取宏任务形成时间环。
Node事件循环
node的事件循环和浏览器的事件循环在node11版本发布后,基本上就一致了,仅有的差异是,node事件循环每个宏任务都有一个单独的回调函数队列,并且按顺序执行。如下:
- timers: setTimeout、setInterval
- pending callbacks: 系统内部调用,不受我们控制
- idle, prepare: 只在系统内部使用
- poll: 存放异步 i/o 操作队列,readFile、writeFile等
- check: 存放setImmediate队列
- close callbacks: 系统内部调用
举个栗子
let fs = require('fs');
fs.readFile('./note.md','utf8',(err,data)=>{
setTimeout(()=>{
console.log('呵呵呵');
}, 0);
setImmediate(()=>{
console.log('setImmediate');
}, 1000)
})
输出:setImmediate、呵呵呵。
因为 readFile处于poll阶段,下一阶段是check阶段,所以先执行setImmediate,然后因为没有微任务,所以再从头执行setTimeout。
本轮循环和次轮循环
异步任务可以分为两种:
- 追加在本轮循环的异步任务。
- 追加在次轮循环的异步任务。
此处的循环就是事件循环,在这边需要理解的是本轮循环一定早于次轮循环。
Node 规定,process.nextTick 和 Promise 的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行他们。而 setTimeout、setInterval、setImmediate 的回调函数,追加在次轮循环。
举个微任务栗子
process.nextTick(()=>{
console.log('nextTick2')
process.nextTick(()=>{
console.log('nextTick3')
process.nextTick(()=>{
console.log('nextTick4')
})
})
})
setTimeout(()=>{
console.log('setTimeout2')
},0)
//输出
nextTick2
nextTick3
nextTick4
setTimeout2
举个大栗子
setTimeout(()=>{
console.log('setTimeout1')
process.nextTick(()=>{
console.log('nextTick1')
})
})
process.nextTick(()=>{
console.log('nextTick2')
setTimeout(()=>{
console.log('setTimeout2')
})
})
let fs = require('fs');
fs.readFile('./note.md','utf8',(err,data)=>{
fs.readFile('./note.md','utf8',(err,data)=>{
console.log(12313)
})
setTimeout(()=>{
console.log('呵呵呵');
}, 0);
setImmediate(()=>{
console.log('setImmediate');
}, 1000)
})
//结果
nextTick2
setTimeout1
nextTick1
setTimeout2
setImmediate
12313
呵呵呵
//或者
nextTick2
setTimeout1
nextTick1
setTimeout2
setImmediate
呵呵呵
12313
输出:nextTick2、setTimeout1、nextTick1、setTimeout2、setImmediate、12313、呵呵呵。
主栈执行:第 7 行入微任务队列。第 1 行入 timer 队列。第 14 行入 poll 队列。
第一轮循环:清空本轮循环微任务队列。输出 nextTick2。第9 行 入 timer 队列,执行一个宏任务(第一行) timer,输出 setTimeout1,第4行入次轮微任务队列。
第二轮循环:清空本轮循环微任务队列执行输出 nextTick1,timer 中宏任务(第9行)执行,输出 setTimeout2。
poll 队列宏任务(第4行)执行, 第18行入 timer队列,第15 行入 poll 队列,第22行入 check 队列,由于当前处于poll 阶段,所以接下去先执行 check阶段,输出 setImmediate,然后是 timer 阶段,但是因为setTimeout 实际上做不到 0毫秒延迟,结果有两种 。(所以先输出 poll 阶段,即 12313,然后是timer阶段输出 呵呵呵)或者(先输出 呵呵呵 再输出 12313)。