[浏览器知识点] 事件循环:宏任务微任务

406 阅读4分钟

事件循环

简单的概念,它是一个在JavaScript引擎中 【等待任务】 【执行任务】进入休眠状态等待更多任务】 这几个状态不断转换无限循环。

引擎的一般算法:

  1. 当有任务时:
  • 从最先进入的任务开始执行。
  1. 休眠直到出现任务,然后转到第 1 步。

设置任务 —— 引擎处理它们 —— 然后等待更多任务(即休眠,几乎不消耗 CPU 资源)。

一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。

多个任务组成了一个队列,即所谓的“宏任务队列”(v8 术语):

队列中的任务基于先进先出的原则执行。

宏任务与微任务

宏任务与微任务都是异步任务,它们属于一个队列。

宏任务:整体代码、setTimeout、 setInterval、 I/O操作、UI rendering ...

微任务:new Promise().then/.catch/.finally、process.nextTick、MutainOberver..

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。流程图可阅读 JS事件循环机制(event loop)之宏任务/微任务查看。

例如下面的示例:

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("code");

这里的执行顺序为:

  1. code 首先显示,因为它是常规的同步调用。
  2. promise 第二个出现,因为 then 会通过微任务队列,并在当前代码之后执行。
  3. timeout 最后显示,因为它是一个宏任务。

再看一个经典面试题:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
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');

白话分析:

从上往下看,先执行第一遍宏任务整体代码块:

async1async2只定义了,还没调用,往下走到整体代码块,输出script start;

接下来 遇到setTimeoutsetTimeout需要等待当前队列中所有的消息都处理完毕之后才能执行,文章参考-零延迟,因此放入下一次宏任务;

接下来 执行async1(),输出'async1 start';下一步执行await async2(),输出async2,这里的await async2()可以理解成new Promise(()=>{async2()}),后面的代码放到then里面。

相当于
new Promise(()=>{
        async2();//await后面的直接放入函数内,属于宏任务
   }).then(()=>{
       console.log(async1 end);//再后面的放在then里面,属于微任务
   })

接下来 遇到new Promise,属于普通任务,输出promise1;

接下来 输出 script end

至此,第一遍宏任务执行完毕,接下来清空微任务队列

第一个微任务:输出async1 end;

第二个微任务:promise2 至此,微任务执行完毕,继续开启第二轮宏任务:即输出 setTimeout

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

详细分析参加:从一道题浅说 JavaScript 的事件循环

再来一题:

console.log('start'); 

setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(()=>{
        console.log('children3');
    })
}, 0);

new Promise(function (resolve, reject) {
    console.log('children4'); // 宏任务
    // 第一轮宏任务结束,尝试清空微任务队列。发现没有微任务,因为resolve('children6')被放入了setTimeout里
    setTimeout(() => {
        console.log('children5');
        resolve('children6') // 通过setTimeout把微任务延迟添加
    }, 0);
}).then((res)=>{
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0);
})

输出:

start
children4
children2
children5
children3
children7
children6

事件循环机制的流程:

  1. 主线程执行JavaScript整体代码,形成执行上下文栈,当遇到各种任务源时将其所指定的异步任务挂起,接受到响应结果后将异步任务放入对应的任务队列中,直到执行上下文栈只剩全局上下文;
  2. 将微任务队列中的所有任务队列按优先级、单个任务队列的异步任务按先进先出的方式入栈并执行,直到清空所有的微任务队列;
  3. 将宏任务队列中优先级最高的任务队列中的异步任务按先进先出的方式入栈并执行;
  4. 重复第 2 3 步骤,直到清空所有的宏任务队列和微任务队列,全局上下文出栈。

简单来说:主线程先执行整体代码块,过程中将遇到的各个任务源所指定的任务分发到各个任务队列中,然后微任务队列、宏任务队列交替入栈执行,直到清空所有的任务队列,全局上下文出栈。

参见