浏览器事件循环

148 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

介绍

众所周知,JS 的是单线程的,或者说,JS 运行进程当中只有一个主线程,无法开辟多个线程池来进行多线程任务。那么按照这样的逻辑,JS 应该是同步执行,从上到下的。那为什么实际上的 JS 和我们理解的不一样呢?在开发当中,我们经常会发送请求,进行异步操作,这是怎么实现的呢??下面我们就来详细的说说。

原理

浏览器事件循环

事件循环之所以被称为事件循环,是因为它经常按照如下的方式来执行:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

queue.waitForMessage()会同步地等待消息的到达(如果当前没有任何消息等待被处理)。

事件循环的主要机制就是任务队列,并且是优先级队列。任务队列可以有多个,优先级也不同,执行队列优先级最高,微任务队列其次,宏任务队列优先级最低。我们接下来分别介绍这三个队列。

执行栈队列

当一个块级作用域执行的时候,会生成一个执行环境(执行上下文),执行环境当中包含了一些当前作用域当中的参数,变量等等,它门会被推入到堆栈当中,始终处于堆栈的顶部,当当前块级作用域执行完成之后,就会被弹出。在执行栈队列当中的所有函数、变量等等都会被同步执行,从上到下。当我们执行到异步函数,比如setTimeout()时,会执行它,但是不会立即返回结果,而是将其加入到微任务队列当中。

微任务队列

在 javascript 当中,由 js 发起的任务称为微任务。在 ES3 以及以前的版本中,JavaScript 本身没有发起异步请求的能力,也就没有微任务的存在。在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器,JavaScript 引擎自身也能够发起异步任务了。在执行完调用栈之后,浏览器会优先执行微任务队列当中的任务。

console.log("调用栈");

setTimeout(function () {
  console.log("宏任务");
}, 0);

const promise = Promise.resolve();

promise.then(() => {
  console.log("微任务");
});

pWI1t.png

从控制台输出的结果可以看出,js 引擎在执行完调用栈当中的任务之后会先执行微任务队列当中的任务。那么有哪些是微任务呢?

  • Promise
  • MutaionObserver
  • process.nextTick(Node.js)
  • queueMicrotask()

通过上面这些函数可以向微任务队列当中添加任务。但是,有一点需要注意,如果可能的话,开发者并不应该过多的使用微任务。在基于现代浏览器的 javascript 开发当中,有一个高度专业化的特性,那就是允许你调度代码跳转到其他事情之前,而那些事情原本处于用户计算机中一大堆等待发生的集合之中的。滥用这种能力将会带来性能方面的问题。

使用微任务的最主要原因简单归纳为:确保任务顺序的一致性,即便当结果或数据是同步可用的,也要同时减少操作中用户可感知到的延迟而带来的风险。

宏任务队列

宏任务队列是通过宿主对象(浏览器/Node)发起的,宏任务主要包含

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O, UI rendering