现代JS学习笔记:理解浏览器事件循环——宏任务和微任务
Posted by Mars . Modified at
学习内容:《现代JavaScript教程》
1.★事件循环
当浏览器没有任务执行时,处于休眠状态。
当任务出现,JS脚本默认情况下是单线程同步执行的,也就是按照出现的先后顺序执行任务,先进入的任务先执行。(任务队列)
事件循环:单线程脚本语言Javascript处理任务的一种执行机制,通过循环来执行任务队列里的任务。一个宏任务执行开始到下一个宏任务执行开始,叫做一次事件循环(一个tick)。
浏览器中的任务分为宏任务和微任务。
1.1 宏任务
以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个队列,按照进入的先后顺序执行,先进先出。
下列任务都属于宏任务:
- 当外部脚本
<script src="...">
加载完成后,执行这个脚本的任务过程,是宏任务; - 用户事件:当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序,这个过程也是宏任务;
- setTimeout/setInterval这类事件:当安排的(scheduled)setTimeout 时间到达时,会产生宏任务,任务就是执行其回调;
- postMessage。
宏任务执行的间隙,如果有微任务,则浏览器先执行微任务,然后执行DOM渲染。
在一个宏任务的执行过程中不进行任何DOM渲染,只有完成后才进行。
1.2 微任务
微任务仅来自于我们的代码。有如下几种形式:
- 由 promise 创建的:在主线程执行过程中,如果遇到promise对象resolve/reject之后,调用了then/catch/finally方法,会立即将这些方法内部的函数作为微任务,添加到微任务队列中(注意必须先resolve/reject);
- async/await函数也会创建微任务:await之后的代码(不包括await所在行)都会作为微任务异步执行(await 当前行的代码会立即执行,它后面的代码作为微任务异步执行,相当于应用了.then方法);
- Generator函数;
- DOM中的MutationObserver触发后的回调函数;
queueMicrotask(func)
,它手动添加 func 到微任务队列。
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后渲染(如果有更新),然后再执行其他的宏任务。
1.3 async/await函数中的宏任务和微任务
async异步函数中的await的含义是:
在此处等待一个异步结果,并阻塞所有后面的函数执行,直到这个结果被获取,再继续执行后面的函数程序。(await后如果是一个函数,也会立即同步执行。)
包括await本身这一部分及其之前的所有函数代码,都是立即同步执行的。但是await
之后的代码并不是,它后面的所有代码会变成微任务,先暂时性转移到另一个队列(微任务队列)中,等待后续再异步执行。
也就是说,浏览器不会等待await返回结果,而是会继续执行async/await函数体外后面的宏任务代码,完毕后再按顺序执行微任务队列中的任务。再之后进行渲染,然后再执行下一个宏任务。
所以结果是一个await把async异步函数内的代码分隔成了两部分:
- 它本身和它前面的部分都是同步执行的;
- 它的回调函数和它后面的async体内代码都是微任务,会按微任务入队顺序选取合适时机依次执行。
1.4 一个经典的案例来理解宏任务和微任务
下面这个经典的代码题,可以供分析参考理解。
// 这段函数会先后输出什么字符串呢?
async function async1() {
console.log('async1 start');
console.log(await async2());
console.log('async1 end');
}
async function async2() {
console.log('async2');
return 'async2 return';
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
需要注意以下几个事实:
- 整个这段代码段在加载后,会被当做一个宏任务来执行;
- 其中的async1是带有await的异步函数;
- 其中的async2是不带await的异步函数;
- setTimeout的回调函数另一个宏任务,会被推送到宏任务队列中;
- Promise的声明本体是同步的,then()函数内的部分是异步的,会被推送到微任务队列。
★★★ 这里的过程如下: (Mars 2021.03.11)
- 整个代码作为宏任务,从上到下执行;
- async1、async2两个函数声明本身没有被执行,无输出;
- console.log(‘script start’)输出第一个字符串
script start
; - setTimeout执行,将一个回调函数function(){console.log(‘settimeout’)}作为下一个宏任务,推入宏任务队列中等待执行;(此处未执行)
- 执行async1函数:await前都是同步的,可以正常立即执行,所以输出第二个字符串
async1 start
。 - 然后,遇见了await关键字。它会立即执行后面的async2函数,然后等待返回结果用于给console.log作参数输出。因此,它在操作执行async2后打断,把后面的所有流程都变成了微任务,推送到微任务队列中等待执行。此处执行了async2函数;
- async2函数内没有await和其他异步行为,直接执行输出第三个字符串
async2
,然后返回async2 return供async1的await在后续的微任务中使用; - 然后是一个Promise对象声明,声明过程本身是同步执行的,所以then之前的代码都立即执行,输出第四个字符串
promise1
; - then()包含的函数代码是需要等待promise被resolve然后异步执行的,属于微任务,被推送到微任务队列等待执行;
- 宏任务代码的最后一行console.log(‘script end’),直接正常执行,输出第五个字符串
script end
。 - 至此,这段脚本作为第一个宏任务执行完毕,在下一个宏任务(第4步setTimeout的回调)执行前,先处理微任务队列。因此此时按先进先执行的原则,执行async1中的剩余部分:输出async2的返回值
async2 return
,然后输出async1 end
; - 执行下一个微任务:promise声明后的then()。因此此时输出第八个字符串
promise2
; - 至此所有微任务执行完毕。可以开始执行下一个宏任务:setTimeout的回调。因此此时输出了第九个字符串
settimeout
。 - 至此全部任务执行完毕,浏览器进入等待状态。