浏览器的事件循环机制?看这一篇就够了

679 阅读8分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战(已经更文7天)

前言

面试中遇到的高频问题当然少不了浏览器的事件循环了,什么是浏览器的事件循环?什么是微任务宏任务?为什么要区分微任务宏任务?给你一段代码让你写出执行的先后顺序等。如果你不懂或者一知半解,这篇文章带你捋清浏览器的事件循环

为什么有事件循环?

众所周知,JavaScript是一门单线程的解释型脚本语言,而单线程也意味着JS代码在执行的时候,只有一个主线程来处理所有的任务。

那么为什么JS在设计之初不像Java一样设计成多线程呢?因为JS的定位是与浏览器做交互,它是一门脚本语言。如果它是多线程的,试想一下,如果在同一时刻,有一个线程操作一个DOM,改变它的样式,而另一个线程也在操作这个DOM,执行删除操作,那么浏览器听谁的呢?

单线程有没有弊端呢?那肯定是有的。我们再试想一下,在主线程中有两个操作A和B,A先执行,B后执行。A如果需要耗时0.2s,那必然是没什么问题的,那如果A要耗时20s、200s、2min呢?那B操作就会一直处于一个等待的状态,那这种结果必然是无法接受的。正是为了解决单线程会造成的阻塞问题,浏览器引入了事件循环机制

什么是事件循环?

JS的任务分为同步任务异步任务

  • 同步任务: 立即执行的任务,在主线程上排队执行,前一个任务执行完毕,才能执行后一个任务。主要有:Promise(then/catch/finally等)、Object.obsever、MutationObsever等。
  • 异步任务: 异步执行的任务,不进入主线程,而是在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候读取执行。主要有:setTimeout、setInterval、I/O、UI渲染

而同步任务都在主线程中执行,构成了一个执行栈。而除了主线程的执行栈之外,还存在一个任务队列,用来存放异步任务执行完毕后注册的回调函数。

执行栈执行同步任务,在遇到异步任务时,会将异步任务进行异步处理并且跳过,把所有同步任务执行完成后,再执行异步处理结束后推入任务队列的异步任务回调函数。而这个过程是循环不断进行的,这就被称为事件循环

image.png

举个栗子:

console.log("script start");

setTimeout(() => {
  console.log("我是异步任务");
}, 100);

console.log("script end");

上下两个console打印是同步任务,所以会先执行,而setTimeout是一个异步任务,所以首先会跳过它,等它的异步处理完后,会把回调放入任务队列中,主线程再执行这个回调。

image.png

打印结果:

image.png

那么,异步任务又都有哪些呢?具体的异步处理又是什么呢?

微任务和宏任务

异步任务也是有区别的,它分为微任务(MicroTask)和宏任务(MacroTask),他们执行的优先级也是有区别的。

当执行一段代码时,会先执行同步任务,当遇到异步任务时,区分它是宏任务还是微任务

如果是宏任务,则会把此宏任务放入一个消息队列中挂起,继续往下执行。

如果是微任务,则会把此微任务放入全局执行上下文中的一个微任务队列中,继续往下执行。

而等到同步任务都执行完毕后,会先去微任务队列中读取微任务执行,等清空了微任务队列后,再去执行消息队列里的宏任务,进入下一次事件循环。

流程图:

image.png

每个事件循环,当执行栈中的同步任务都已经执行完毕后,会去检查微任务队列,如果其中有待执行的微任务,则会按照先进先出的方式依次执行微任务。等到微任务队列被清空后,才会去消息队列中读取宏任务,进入下一次事件循环。(微任务的优先级是高于宏任务的

image.png

为什么要区分微任务和宏任务?

试想一下,假如不区分微任务或者宏任务,只有一个广义上的异步任务会怎么样?

假如你有一个异步任务A,其内部还有一个异步任务B。当同步任务都执行完后,开始调用异步任务A,发现它其中还有一个异步任务B。这时候因为不区分异步任务,所以又把异步任务B给挂起了,等下一次事件循环再去调用它。这显然是不合理的,因为可能后面的状态需要用到异步任务B,你的目的就是在这一次事件循环中都执行完这两个异步任务。

举个具体的生活中的例子,你去食堂排队打饭,把你自己当作一个异步任务。好不容易排队到你了,你打完了菜,需要刷卡付钱。而刷卡付钱这个操作也是一个异步任务,这时候如果不区分微任务或者宏任务,你就需要再回过头去排队,等到你再次排到了才能付钱(当然不能白嫖了,所以点个赞再走吧~)。这显然是不合理的。

所以,我们需要区分微任务或者宏任务,形成一个具有优先级的关系,就是为了方便插队。在一次事件循环中,先执行优先级较高的异步任务(插队),再去执行优先级较低的异步任务。

来个栗子

现在我们了解了 浏览器的事件循环机制 ,我们就动手来看个面试题试试。

我们来看看它执行完后输出什么:

console.log("script start");

new Promise((res) => {
  console.log(1);
  res();
}).then(() => {
  console.log("微任务1");
});

const p = new Promise((res) => {
  console.log(9);
  res();
}).then(() => {
  console.log("微任务2");
});

async function foo() {
  console.log("async start");
  await p;
  console.log("async end");
}

foo();

setTimeout(() => {
  console.log("宏任务1");
  new Promise((res) => {
    console.log(3);
    res();
  }).then(() => {
    console.log("微任务3");
  });
}, 101);

setTimeout(() => {
  console.log("宏任务2");
  new Promise((res) => {
    console.log(2);
    res();
  }).then(() => {
    console.log("微任务4");
  });
}, 100);

console.log("script end");
  1. 首先将整体代码作为一个宏任务执行,同步任务console.log进入执行栈,执行打印操作后弹出执行栈,打印了script start

image.png

  1. 走到第一个Promise中,Promise是一个微任务,但是它的构造函数入参是一个同步任务,console.log(1)进入执行栈,输出一个1后弹出执行栈。紧接着调用了resolve函数,而Promise.then是一个微任务,所以它被放入了微任务队列当中,暂时挂起,而后resolve函数弹出执行栈。

image.png

// new Promise中是一个同步任务
new Promise((res) => {
  console.log(1);
  res();
})
  1. 走到第二个Promise中,跟前一个Promise一样先打印一个9,再调用resolve函数,挂起Promise.then微任务队列中。

image.png

  1. 执行foo()函数,它是一个async函数(点这里了解async),先执行console.log打印,走到了await p处之后,就相当于new Promise(()=>{p}),异步转同步,但是并不会发生什么,因为p已经执行过了,但是它会把下面的代码放入new Promise(()=>{p}).then中,导致console.log("async end")也被推入微任务队列中。

image.png

  1. 接着执行setTimeout,它是一个宏任务,所以将它放入消息队列中挂起。

image.png

  1. 同上,挂起第二个setTimeout

image.png

  1. 执行console.log("script end")

image.png

  1. 到现在,这段代码的同步任务都已经执行完了,现在微任务队列中有三个微任务挂起,消息队列中有两个宏任务挂起。之前提到过,微任务的优先级高于宏任务,所以会先执行微任务队列中的任务,顺序是先进先出(队列),所以依次打印并且清空微任务队列,到这里,第一轮事件循环结束,开启第二轮。

image.png

  1. 微任务队列清空后,会去消息队列执行宏任务,而现在有的宏任务是两个setTimeout,该执行哪个呢?是按照队列先进先出的规则先执行101ms的那个吗?不是的,setTimeout会根据时间来执行,谁先到时间先执行谁,比如现在已经到了100ms,它就会先执行100ms的那个,再执行101ms的那个。所以,先进入100ms的setTimeout中,先后打印宏任务22,后挂起里面的微任务。

image.png

  1. 接着执行微任务,等清空微任务队列后,再跟上面一样执行101ms的setTimeout,最终打印结果:

image.png

  1. 我们去浏览器打印看看具体是什么。

image.png

结语

浏览器的事件循环机制在面试中是高频的问题,相信这篇文章或多或少会给予您一些帮助,如果看完这篇文章,对您有所帮助的话,还烦请您点个赞,点点关注,祝您生活愉快。