宏任务,微任务和EventLoop
这么长时间的前端学习,我们有没有好好了解过我们最熟悉的,同时也是最陌生的浏览器,今天我们就来看看到底我们浏览器内部是怎么为我们执行我们的指令的。
javaScript是单线程的语言
首先,我们要知道javaScript是一门单线程的脚本语言,这个是什么意思呢?
就是在一行代码执行过程中,必然不会存在同步执行的另一行代码。所以当代码全部是同步执行的时候,将会有很混乱的问题,比如我们从远端获取一些数据, 如果立即获取数据的话,这个时候是无法获取到数据的。
所以就有了异步事件的概念,注册一个回调函数,发一个网络请求,我们告诉主程序等到接收到数据之后执行相应的操作,然后我们这段时间就可以去执行其他的操作了。异步完成后,会执行到相应的操作。
宏任务和微任务的区别
举个🌰(例子):
在我们去银行办业务的时候,我们往往都会先去排队取号,取号排队的每一个客人都是一个宏任务,当柜员处理完一个客户的问题之后,就会叫下一个客户,这也是下一个宏任务的开始。
多个宏任务合在一起就可以看做是一个任务队列在这,里面存的是银行内所有正在排号的客户。
任务队列中的都是已经完成的异步操作,而不是注册一个异步任务都会被放在这个任务队列中,就像在银行中排号,如果轮到你的号的时候,你不在,那么你的号就作废了,回来的时候还需要重新排队。
我们每一个人办完业务之后,柜员可能会问我们是否还需要投资新的其他的理财项目,如果我们有想法的话,柜员肯定不会让我们去旁边再次排队取号的,比如买点纪念币,办点理财等等,这些都可认为是微任务。
当前的微任务没有执行完毕之前,不会执行下一个宏任务。
我们现在来看看经常出现在面试和许多大佬的blog中的一个代码片段:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
面试管经常可能会问我们这段代码会输出什么?
setTimeOut是作为宏任务存在的,而promise.then/catch/finally则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的,因为new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步进行的。
所以,我们需要明白哪些是宏任务,哪些是微任务就变得很关键。
宏任务
| # | 浏览器 | Node |
|---|---|---|
I/O |
Y | N |
setTimeOut |
Y | N |
setInterval |
Y | N |
setImmediate |
N | Y |
requestAnimationFrame |
Y | N |
UI事件 |
Y | Y |
微任务
| # | 浏览器 | Node |
|---|---|---|
process.nextTick |
N | Y |
MutationObserver |
Y | N |
promise.then catch finally |
Y | Y |
Object.observe |
Y | Y |
Event-Loop
我们有了宏任务,微任务的概念,现在我们来看看到底什么东东是Event-Loop。
在浏览器执行任务的过程中,有很多的宏任务,也有很多的微任务,什么时候执行宏任务,什么时候执行微任务就是我们该考虑的,也需要有一个判断逻辑存在。
在处理玩一个任务之后,柜员就会问是否还有其他需要办理的业务(检查还有没有微任务需要处理);客户说没有事请之后,柜员就去查看还有没有别的客户等着办业务(检查还有没有宏任务需要处理)。这个检查的过程是持续进行的,每完成一个任务就会进行一次,这样的操作就叫做Event-Loop。
浏览器中的表现
在浏览器中,宏任务是在微任务之后才执行的(其实微任务实际上是宏任务的其中一个步骤)。

流程讲解如下:
浏览器执行任务前会判断执行的任务是同步任务还是异步任务;
同步任务则会一直执行到结束;
遇到异步就将它交给对应的异步队列,自己执行同步任务;
异步线程执行异步
API,执行完成后,将异步回调事件放入事件队列上;主线程的同步队列任务执行完之后回去事件队列看有没有任务;
主线程发现事件队列有任务,救取出里面的任务执行;
主线程不断循环上述流程;

- 一个Event Loop可以有一个或多个事件队列,但是只有一个微任务队列。
- 微任务队列全部执行完会重新渲染一次
- 每个宏任务执行完都会重新渲染一次
requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列
Node.js的Event-Loop
Node.js是运行在服务端的js,所以他的服务目的和运行环境不同,导致了它的API和原生JS有些不同,它的Event-Loop还需要处理一些I/O,所以与浏览器Event-Loop也是不一样的。Node的Event-Loop是分阶段的,如下图所示:

- timers:执行
setTimeOut和setInterval的回调 - pending callbacks:执行延迟到下一个循环迭代的
I/O回调 - idle,prepare:仅系统内部使用
- poll:检索新的I/O事件;执行相关I/o的回调,事实上除了其他几个阶段的事情,几乎所有的异步都在这个阶段处理
- check:
setImmdiate在这里执行 - close callbacks:一些关闭的回调函数,如
socket.on('close',...)

setImmdiate和setTimeout
官方文档的定义,setImmediate为一次Event-Loop执行结束后调用;
setTimeout是计算一个延迟时间后进行的执行。
setTimeout(()={
setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))
},0);
大家有兴趣的可以自己下去试试,这两行代码执行多次会得到不同的值。
啊啊啊,这我刚看懂了前面的又来了这个东西,怎么又出现了这个东西。我们来慢慢分析一下这个小东西。
- 外层是一个
setTimeout,所以执行的他的回调的时候已经在timers;- 处理里面的
setTimeout,因为本次循环的timers,正在执行,所以他的回调其实是加到了timers;- 处理里面的
setImmdiate,将它的回调加入check阶段队列- 外层的
timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下- 到了
check阶段,发现了setImmdiate的回调,拿出来执行- 然后是
close callbacks,队列是空的,跳过- 又到了
timers阶段,执行我们的打印
process.nextTick()
process.nextTick()是一个特殊的API,遇到process.nextTick()的时候,Event-Loop会立即停下来执行process.nextTick(),执行完之后才会继续执行Event-Loop。
希望大家能有所指导,俺小菜鸡的理解还比较浅。