js event loop事件队列详解(包含async在事件队列中的执行)

877 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

js代码的执行顺序的面试题你还是会做错吗?js代码的运行顺序到底是怎样的呢?

这就是event loop事件队列知识,本文介绍平时使用到的在浏览器中的js event loop事件队列,包括微队列、宏队列的知识,介绍了几个具体实例,还有关于async在事件队列中的执行。

认识一个栈两个队列


一个调用栈Stack。 一个宏队列,macrotask,也叫tasks。 一个微队列,microtask,也叫jobs。

执行过程


js就是执行全局Script同步代码,这中间碰到一些异步任务先加进对应的队列。

做完之后,调用栈就为空了。

然后将队列(先微队列后宏队列)里面的首个任务提到调用栈来做,一件一件做完直到队列中的任务都做完。

总结就是,先做同步的任务,再做微队列的任务,再做宏队列的任务。

异步任务怎么分配


这些异步任务包括但不限于:

以下分配给宏队列

  1. setTimeout
  2. setInterval
  3. requestAnimationFrame
  4. I/O
  5. UI rendering

以下分配给微队列

  1. Promise
  2. Object.observe
  3. MutationObserver

常见的宏队列:setTimeoutsetInterval,常见的微队列:Promise

简单例子


    console.log("同步任务1");

    setTimeout(() => {
      console.log("宏任务");
    });

    new Promise((resolve, reject) => {
      console.log("同步任务2");
      resolve("微任务");
    }).then((data) => {
      console.log(data);
    });

    console.log("同步任务3");

结果是(按标号加任务,按箭头执行):

>

在这里插入图片描述

需要注意的是Promise的第一层没有执行回调之前是同步的,也就是上面的同步任务2

难一点的例子


    console.log("同步任务1");

    console.log("同步任务2");

    new Promise((resolve, reject) => {
      console.log("同步任务3");
      setTimeout(() => {
        console.log("宏任务1");
        Promise.resolve()
          .then(() => {
            console.log("微任务5");
          })
          .then(() => {
            console.log("微任务6");
          });
      });
      resolve("微任务1");
    })
      .then((data) => {
        console.log(data);
        return "微任务3";
      })
      .then((data) => {
        console.log(data);
      });

    setTimeout(() => {
      console.log("宏任务2");
    }, 0);

    new Promise((resolve, reject) => {
      resolve("微任务2");
    })
      .then((data, resolve) => {
        console.log(data);
        return "微任务4";
      })
      .then((data) => {
        console.log(data);
      });

    console.log("同步任务4");

如何看呢,先看第一层,红色代表同步,绿色微任务,蓝色宏任务。

我们会把同步任务执行完,然后看见微任务有俩,宏任务也有俩。

本来的执行顺序可能是这样(我这里按照序号来表达顺序了,请和简单例子区分开来): 在这里插入图片描述 但是没那么顺利,执行到标号6时不一样了。

因为微任务执行过程中可能会产生新的微任务

上面的微任务1执行完会把微任务3加在微任务2后面,也就是微任务2执行完也轮不到宏任务,会继续执行新的微任务直到微任务队列暂时为空。

所以接下来会按照加入队列的顺序执行完四个微任务,这时候发现没有新的微任务产生,才开始执行宏任务:

在这里插入图片描述

但是需要注意的是,上面执行到标号5时又不一样了,宏任务一执行后又产生了新的微任务,所以宏任务两个并没有顺利连续执行,而是被插入的微任务拦住了。

要记住微任务与宏任务队列都存在时一定是微任务先执行完再来执行宏任务,即使是宏任务执行产生的微任务也同理

在这里插入图片描述

所以最后的答案,如果存在不理解的,可以在认真回顾一下上文:

在这里插入图片描述

async/await 在事件队列中如何执行


简单例子

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2 end');
}

async1()

async1函数,思考一下await async2()是干嘛的?

await是不是在等待async2函数完成再进行下一步,它阻塞了,console.log('async1 end')是在它阻塞结束之后完成的

await async2()是不是可以看作一个Promise,而console.log('async1 end')是在它.then之后完成的。

就可以转化为:

async function async1() {
    console.log('async1 start');
    new Promise(resolve => {
        async2()
        resolve()
    }).then(() => {
        console.log('async1 end');
    })
}

function async2() {
    console.log('async2 end');
}

async1()

合并两个函数:

async function async1() {
    console.log('async1 start');
    new Promise(resolve => {
        console.log('async2 end');
        resolve()
    }).then(() => {
        console.log('async1 end');
    })
}

async1()

难一点的例子

有一道很火的面试题,遇到async/await我们又该如何执行呢?

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2 start');
  return new Promise((resolve, reject) => {
    resolve();
    console.log('async2 promise');
  })
}

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);  

async1();

new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
}).then(function() {
  console.log('promise3');
});

console.log('script end');

首先我们要把我们不熟悉的尝试转化为我们熟悉的。

关键的代码是使用了async/await的这部分:

// async1
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
// async2
async function async2() {
    console.log('async2 start');
    return new Promise((resolve, reject) => {
        resolve();
        console.log('async2 promise');
    })
}
async1()

首先我们先看async2,我们知道async函数的返回值是Promise,但是async2原本返回了一个Promise,是这个Promise吗?当然不是,其实async2是返回了一个嵌套的新的Promise,所以代码等价于:

// async2
function async2() {
    console.log('async2 start');
    return new Promise(resolve2 => {
        new Promise((resolve, reject) => {
            resolve();
            console.log('async2 promise');
        }).then(() => resolve2())
    })
}

resolve2是要在原本返回的Promise的resolve之后完成的。

虽然等价,但是将以上直接取代async2是不对的,因为它return了一个Promise,在async1函数中await使用,我们还需去修改async1。

通过简单例子我们可以知道我们需要将await async2()转化为Promise,但是他有点不一样

// 错误❌

function async1() {
    console.log('async1 start')
    new Promise(resolve1 => {
        // async2()返回的是Promise,因此resolve1的完成应该在async2的回调之后
        async2()
        resolve1()
    }).then(() => {
        console.log('async1 end')
    })
}

// 正确✔

function async1() {
    console.log('async1 start')
    new Promise(resolve1 => {
        async2().then(()=>resolve1())
    }).then(() => {
        console.log('async1 end')
    })
}

现在原来的题目就变成了:

function async1() {
    console.log('async1 start')// 同步任务2
    new Promise(resolve1 => {
        async2().then(() => resolve1())// 微任务3
    }).then(() => {
        console.log('async1 end')// 微任务5
    })
}

function async2() {
    console.log('async2 start');// 同步任务3
    return new Promise(resolve2 => {
        new Promise((resolve, reject) => {
            resolve(); // 进入.then
            console.log('async2 promise'); // 同步任务4
        }).then(() => resolve2())// 微任务1
    })
}

console.log('script start');// 同步任务1

setTimeout(function() {    
  console.log('setTimeout');// 宏任务1
}, 0);  

async1();

new Promise(function(resolve) {
  console.log('promise1'); // 同步任务5
  resolve();// 进入.then
}).then(function() {
  console.log('promise2');// 微任务2
}).then(function() {
  console.log('promise3');// 微任务4
});

console.log('script end');// 同步任务6

同样你可以合并两个函数:


function async1() {
    console.log('async1 start')// 同步任务2
    new Promise(resolve1 => {
        console.log('async2 start');// 同步任务3
        new Promise(resolve2 => {
            new Promise((resolve, reject) => {
                resolve(); // 进入.then
                console.log('async2 promise'); // 同步任务4
            }).then(() => resolve2())// 微任务1
        }).then(() => resolve1())// 微任务3
    }).then(() => {
        console.log('async1 end')// 微任务5
    })
}

console.log('script start');// 同步任务1

setTimeout(function() {    
  console.log('setTimeout');// 宏任务1
}, 0);  

async1();

new Promise(function(resolve) {
  console.log('promise1'); // 同步任务5
  resolve();// 进入.then
}).then(function() {
  console.log('promise2');// 微任务2
}).then(function() {
  console.log('promise3');// 微任务4
});

console.log('script end');// 同步任务6

所以console.log('async1 end')在微任务队列中排第五,输出在console.log('promise2')console.log('promise3')之后,如果感兴趣,可以多添加promise4、promise5再看看结果。

尾言

如果觉得文章对你有帮助的话,欢迎点赞收藏哦,有什么错误或者意见建议也可以留言,感谢~