JS的宏任务、微任务和EventLoop
JS
是一个单线程脚本语言,这是大家公知的。单线程的设计与其用途都是有关系的,因为有很多的场景需要保证同一时间只能做一件事情。比如DOM
的创建与删除等等。
当然为了利用多核CPU
的算力,HTML5
提出的Web Worker
标准,允许JS
脚本创建多个线程,但是子线程完全需要听从主线程的安排。所以本质上是没有改变JS
单线程的事实。
任务队列
单线程只能保持同一时间只能做一件事情,那么所有的任务需要排队执行,前一个任务结束,下一个任务才能执行。如果前一个任务耗时很长时间,后一个任务就需要等着。
绝大多数,现在的计算机算力都是性能过剩的,也就是CPU
很多的时候都是空闲的,但是由于受限于I/O
,需要等待结果出来再执行。
JS
设计者也意识到这个问题的发生,也就是主线程完全可以不管I/O
设备,挂起等待的任务,先运行排在后面的任务。等到I/O
返回了结果,在等到可以执行的时候,将挂起的任务再执行下去。
于是在JS
中任务就分为:同步任务,异步任务。同步任务指的是:在主线程上排队执行的任务,它存在于任务栈中,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是:不进入主线程,而进入任务队列,只有任务队列通知主线程某个异步任务可以执行,那么这个异步任务才会进入主线程执行。
在JS
中我们可以将同步执行视为没有异步任务的异步执行,那么所有都归类到异步执行下,可以简单这么总结一下js
执行任务的机制:
- 同步任务都是在主线程上的执行栈(
stack
)中执行 - 主线程以外的任务队列。只要异步任务有了运行结果,就会在任务队列中放置一个待执行事件
- 当执行栈中的同步任务都执行完毕,就会去执行任务队列的任务事件,此时任务队列的事件结束等待状态,调入执行栈开始执行。
- 主线程不断的执行上述三步
Event Loop
它是一个任务的执行模型,也可以称为“消息线程”,它用于主线程与“任务队列”进行通信,其目的就是为了减少多线程的等待时间,防止资源的浪费。主线程从“任务队列”中执行事件,这个过程是循环不断的,所以整个运行机制又称为Event Loop
(事件循环)。在浏览器端和NodeJS
环境下都有不同的实现方法。
- 浏览器的Event Loop是
HTML5
规范中明确定义 NodeJS
的Event Loop是基于libuv
实现的
请看下图。
上图中,主线程运行的时候,产生堆(heap
)栈(stack
),栈中的代码调用各种外部API
,他们在任务队列中加入各种事件。当栈中的代码执行完毕后,主线程就回去读取任务队列,依次执行事件所对应的回调函数。
栈(stack
)中的代码执行的是同步任务,总是在异步任务(读取任务队列的事件)之前执行。当然,异步任务也有执行先后顺序之分,分为宏任务与微任务,如图:
这里我们就能看到其实这张图就是对上一张图的补充,同步任务的执行栈中在执行完毕以后会执行回调函数等异步任务,但是此时的异步任务根据不同的关键词分成了不同的任务级别,从而决定了任务的执行顺序。这么做的目的就是为了让所以的异步任务在宏观上也是同步执行的。这里面就提出了两个概念宏任务与微任务
宏任务与微任务
执行过程
先执行微任务,再执行宏任务
宏任务
macroTask
,也叫tasks
,主要的工作如下:
- 创建主文档对象,解析
HTML
,执行主线或者全局的JS
的代码,更改URL
以及各种事件。 - 页面加载,网络事件,定时器等等。
宏任务在浏览器环境下其实就是一个个离散的独立的工作单元,是比较大的任务集合。一些异步任务的回调会进入宏任务队列,这些异步函数包括: setTimeout
setInterval
setImmediate
(node
环境)requestAnimationFrame
(浏览器)I/O
UI rendering
(浏览器)
微任务
microTask
,也叫jobs
,主要的工作如下:
- 微任务更新应用程序的状态,但是必须在浏览器任务继续执行其他任务之前执行
- 微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的
UI
的重绘,因为重绘会使得应用状态的不连续
微任务像是掺杂在各个宏任务之间的微小单元,是相对比较小的任务。一些异步任务的回调会进入微任务队列,这些异步函数包括:
process.nextTick(node)
Promise.then()
catch
finally
Object.observe
MutationObserver
这里我们需要注意点
new Promise(executor()).then(onResolved,onRejected)
,其中executor
是同步执行函数,then
中执行的回调函数onResolved
,onRejected
才是微任务,同时then
的后面再接then
的时候,第二个then
的状态受制于第一个then
返回的结果,在Promise
的链式调用中,每执行一个方法执行完毕都会返回Promise
。
补充:NodeJS中的Event Loop
NodeJS
也是单线程的Event Loop
,但是它的运行机制不同于浏览器环境。
解释:
V8
引擎解析JS
脚本,并调用API
libuv
库负责Node API
的执行。它将不同的任务分配给不同的线程,形成一个Event Loop
(事件循环),以异步的方式将任务的执行结果返回给V8引擎。V8
再将结果返回给用户
在宏任务执行栈中,process.nextTick
方法可以在当前”执行栈”的尾部—-下一次Event Loop
(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate
方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop
时执行,这与setTimeout(fn, 0)
很像。
process.nextTick(function a(){
console.log(1)
process.nextTick(function b(){
console.log(2)
})
})
setTimeout(function c(){
console.log('cccc')
},0)
// 1
// 2
// cccc
上述代码中,由于process.nextTick
方法指定的回调函数,总是在当前“执行栈”的尾部触发,所以不仅函数a
比setTimeout
指定的回调函数c
先执行,而且函数b
也比c
先执行。这说明如果有多个process.nextTick
语句(不管是否嵌套),将全部在当前的执行栈底执行,所以关于 process.nextTick
,就只需要记住一点,那就是 process.nextTick
优先于其他的微任务执行。
再看setImmediate
setImmediate(function a() {
console.log(1);
setImmediate(function b(){console.log(2);});
});
setTimeout(function timeout() {
console.log('cccc')
}, 0)
setImmediate
与setTimeout(fn,0)
各自添加了一个回调函数a
和c
,都是在下一次Event Loop
触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–cccc–2
,也可能是cccc–1–2
。因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate。
Node.js
文档中称,setImmediate
指定的回调函数,总是排在setTimeout
前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){
setImmediate(function a() {
console.log(1);
setImmediate(function b(){console.log(2);});
})
setTimeout(function c() {
console.log('cccc')
}, 0)
})
// 1
// TIMEOUT FIRED
// 2
setImmediate
和setTimeout
被封装在一个setImmediate
里面,它的运行结果总是1–cccc–2
,这时函数a
一定在c
前面触发。至于2
排在cccc
的后面(即函数b
在c
后面触发),是因为setImmediate
总是将事件注册到下一轮Event Loop
,所以函数a
和c
是在同一轮Loop
执行,而函数b
在下一轮Loop
执行。
事实上,现在要是你写出递归的process.nextTick
,Node.js
会抛出一个警告,要求你改成setImmediate
。
关于 async/await 函数
async/await
本质上还是基于Promise
的一些封装,而Promise
是属于微任务的一种。所以在使用await
关键字与Promise.then
效果类似
setTimeout(() => console.log(4))
async function main() {
console.log(1)
await Promise.resolve()
console.log(3)
}
main()
console.log(2)
// 1,2,3,4
可以理解为,await
以前的代码,相当于与 new Promise
的同构代码,以后的代码相当于 Promise.then
。
JS的宏任务与微任务以及Event Loop机制可以让我在执行代码的时候更加清晰的知道其结果让我们能够更好的理解JS,当然关于这方面的面试题也是很多的,比如
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
[相关参考:]