Event Loop 事件循环
Event Loop是nodejs当中特有的概念, 跟JS没有特定的关系 浏览器当中的事件处理和Event Loop没有关系
Event Loop到底是个什么东西呢
Event Loop我认为它是nodejs当中的一套完整的处理回调的机制, 它是运行在脱离于JS执行线程之外的, 它是处理Nodejs当中所有的回调, 包括常见的读文件, 写文件, 计时器等等
Event Loop这套机制是C++实现的
它是由各个阶段组成的, Event Loop中文含义就是事件循环, 出现循环二字差不多都是一套流程反复执行的意思 就好比人的一生
生 ==> 老 ==> 病==> 死==> 投胎==> 生==> 老 ==> 病………..
其实人的一生也是在不停的循环, 在每个阶段做每个阶段应该做的事
Event Loop也是一样的
Event Loop的几个阶段
timers
pending callbacks
idle, prepare
poll
check
close callbacks
这是nodejs官网给出的大体示意图, 具体文末给出了链接点击查看
上面的6个阶段依次执行, 循环往复
如果我们只想弄清楚我们平时工作所遇到的一些困惑setTimeout setImmediate, nextTick等这些回调执行先后的顺序问题, 我们可以简化一下上面的阶段
timer
poll
check
只研究这三个阶基本可以弄清执行顺序问题了
下面就依次说一下这三个阶段都在干什么
- timer setTimeout添加的回调函数并指定时间, 含义是在某某时间后执行这个回调,而不是在某某固定时间执行这个回调 这个阶段就是执行setTimeout添加的回调函数
- poll
轮询阶段的功能
- 如果发现计时器的时间到了, 就会绕回timer阶段执行计时器的回调函数, 在绕回timer阶段的过程中, 会路过check阶段
- 如果计时器时间没到, 就执行poll阶段队列里面的回调
- 如果 poll 队列不是空的,event loop 就会依次执行队列里的回调函数,直到队列被清空或者到达 poll 阶段的时间上限
- 如果 poll 队列是空的
- 如果有setImmediate任务, event loop就结束poll阶段, 进入check阶段,
- 如果没有setImmediate任务event loop 就会等待新的回调函数进入 poll 队列,并立即执行它
一旦poll队列为空, event loop就轮询检查有没有计时器到达时间, 如果有就绕回timer阶段, 去执行计时器回调函数
- check 这个阶段允许开发者在 poll 阶段结束后立即执行一些函数, 如果 poll 阶段空闲了, 同时存在** setImmediate**任务,event loop 就会进入 check 阶段
只要弄清了这三个阶段就可以弄清异步函数执行的时机了
setImmediate vs setTimeout
两者很像, 但是其回调的执行时机有很大的不同 参考上面三个阶段的说明
从三个阶段的说明大体应该可以分析出, event loop大部分都处于** poll阶段 那么就假设我们执行 setImmediate和 setTimeout时就处于 poll**阶段 分析这段代码
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
如果按照上面所说的, 应该是先打印immediate
然后打印timeout
因为处理poll阶段时发现有计时器到达时间, 那么就绕回timer阶段, 在路过check阶段时发现有setImmediate添加的回调, 那么就执行这个回调
但事实并非如此, 因为这取决去执行JS代码线程的性能, 再说的具体一些就是event loop开启的时机不能保证,所以不一定就在poll阶段, 也有可能在别的阶段, 所以执行的顺序是不能保证的
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
如果是这样就可以保证了执行顺序, 原因也就是上面分析的 如果在开启event loop之前,这时JS代码已经执行完了,则开始就处于timer阶段, 这时发现timer阶段有一个0ms后执行的回调(可能这个回调已经超过0ms了,过期的回调就更要立马执行掉了), 那么就执行这个回调, 所以setTimeout添加的回调可能是比setImmediate添加的还要先执行
nextTick
其实nextTick理解起来更简单一些, 可以简单地粗暴的认为nextTick添加的回调无论处于哪个阶段, 都会在这个阶段结束之前将nextTick添加的回调执行完毕
浏览器的微任务和宏任务
其实浏览器的微任务和宏任务和nodejs的event loop在技术层面没有任何关系
这完全就是两个东西, 不要混在一起 宏任务和微任务是两个不同的任务队列
在浏览器中宏任务就是setTimeout添加的回调, 然后将这个回调放入宏任务队列中 微任务就是promise.then添加的回调, 然后将这个回调放入微任务的队列 await就是promise的语法糖, 所以也是微任务, 还有一个不常用的MutationObseve, Vue的nextTick就是用的这个实现的还有一个废弃的Object.observe,这些都是微任务
那么怎么理解微任务和宏任务呢 我们可以用生活中我们处理事情时的紧急程度的形容词来对应
我们平时说的马上就做就是等我把手里的这点任务完成之后就做 我们说的一会在做就是我得把我手里的任务完成之后,下午在做 手里的任务相当于同步代码
这个形容不是很恰当,但是可以描述微任务和宏任务的两个执行的先后关系了
如果同步代码执行完成之后,微任务是先于宏任务开始执行的
如果微任务和宏任务是交替添加的呢 其实这种情况也好理解 举个🌰
async function async1(){
console.log('1')
await async2()
console.log('2')
}
async function async2(){
console.log('3')
}
console.log('4')
setTimeout(function(){
console.log('5')
},0)
async1();
new Promise(function(resolve){
console.log('6')
resolve();
}).then(function(){
console.log('7')
})
console.log('8')
先说答案 4 1 3 6 8 2 7 5 说一下分析过程
首先所有同步代码执行完毕肯定实现打印4 可以认为async1后面的所有代码都是作为promise.then的回调函数内部了 继续分析 打印完4, 执行了async1 在执行async1之前, setTimeout添加了一个宏任务 然后打印1 又执行了async2 那么就打印3 因为async2也是await的 所以async2后面的代码相当于then中回调函数的代码 被添加进微任务队列 这时async1执行完毕 new Promise的函数参数是立即执行的 所以打印6 然后的then添加了一个微任务 这时new Promise执行完了 打印8
现在的宏任务有一个回调, 微任务有两个回调 先执行微任务队列的回调打印2然后打印7 然后是宏任务队友打印5
建议看英文原版的, 这样应该可以理解的更好一些