现代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 微任务

微任务仅来自于我们的代码。有如下几种形式:

  1. 由 promise 创建的:在主线程执行过程中,如果遇到promise对象resolve/reject之后,调用了then/catch/finally方法,会立即将这些方法内部的函数作为微任务,添加到微任务队列中(注意必须先resolve/reject);
  2. async/await函数也会创建微任务:await之后的代码(不包括await所在行)都会作为微任务异步执行(await 当前行的代码会立即执行,它后面的代码作为微任务异步执行,相当于应用了.then方法);
  3. Generator函数;
  4. DOM中的MutationObserver触发后的回调函数;
  5. 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)

  1. 整个代码作为宏任务,从上到下执行;
  2. async1、async2两个函数声明本身没有被执行,无输出;
  3. console.log(‘script start’)输出第一个字符串script start
  4. setTimeout执行,将一个回调函数function(){console.log(‘settimeout’)}作为下一个宏任务,推入宏任务队列中等待执行;(此处未执行)
  5. 执行async1函数:await前都是同步的,可以正常立即执行,所以输出第二个字符串async1 start
  6. 然后,遇见了await关键字。它会立即执行后面的async2函数,然后等待返回结果用于给console.log作参数输出。因此,它在操作执行async2后打断,把后面的所有流程都变成了微任务,推送到微任务队列中等待执行。此处执行了async2函数;
  7. async2函数内没有await和其他异步行为,直接执行输出第三个字符串async2,然后返回async2 returnasync1await在后续的微任务中使用;
  8. 然后是一个Promise对象声明,声明过程本身是同步执行的,所以then之前的代码都立即执行,输出第四个字符串promise1
  9. then()包含的函数代码是需要等待promise被resolve然后异步执行的,属于微任务,被推送到微任务队列等待执行;
  10. 宏任务代码的最后一行console.log(‘script end’),直接正常执行,输出第五个字符串script end
  11. 至此,这段脚本作为第一个宏任务执行完毕,在下一个宏任务(第4步setTimeout的回调)执行前,先处理微任务队列。因此此时按先进先执行的原则,执行async1中的剩余部分:输出async2的返回值async2 return,然后输出async1 end
  12. 执行下一个微任务:promise声明后的then()。因此此时输出第八个字符串promise2;
  13. 至此所有微任务执行完毕。可以开始执行下一个宏任务:setTimeout的回调。因此此时输出了第九个字符串settimeout
  14. 至此全部任务执行完毕,浏览器进入等待状态。
Keywords: JavaScript
previousPost nextPost
已经有 1000000 个小伙伴看完了这篇推文。