浏览器下的事件循环

132 阅读4分钟

前言

在讨论事件循环之前,其实是想介绍 V8 引擎与浏览器之间的联系,奈何学艺不精,网上的资料也是参差不齐,所以不敢侃大山。

不过还是可以确定以下几点:

  • 浏览器提供了 JS 代码运行的额外环境,如 window 对象、事件循环模型。
  • V8 引擎只负责执行 JS 代码,即调用栈中有啥就执行啥,至于代码什么时候、什么顺序入栈由浏览器控制。

事件循环的作用

众所周知,JavaScript 是单线程运行的,这就意味着执行像 setTimeout 中的回调函数,如果没有一个机制让代码继续执行,那么 JS 程序只能阻塞等待函数执行。

Event Loop(事件循环)就是用来解决这问题,这同样也是 JS 实现异步的原因。

阻塞/非阻塞 同步/异步

既然提到了这个概念,作者这里再次声明:专业术语的概念往往不是绝对的,特别是当其应用在广泛的领域。

对于计算机语言领域,阻塞/非阻塞往往等同于同步/异步,最常用于 IO 操作,如对文件进行写入操作。如果是阻塞式的,那么就会等待写入完成才能继续后面的代码。如果是非阻塞的,会继续执行代码,只不过在写入结束时会产生相应信号来告知写入完成。

事件循环的组成

事件循环模型由 3 个部分组成:调用栈、宏任务队列、微任务队列。

栈和队列都是常见的数据结构,这里不多做介绍。

对于浏览器来说 MacroTask(宏任务)主要包括 setTimeout、setInterval、requestAnimationFrame,MicroTask(微任务)主要包括 Promise。

事件循环的进程模型:

  1. 判断宏任务队列是否为空,如果不为空,推出任务放入调用栈,执行代码。
  2. 判断微任务队列是否为空,如果不为空,推出任务放入调用栈,执行代码,再次重复该步骤,直到微任务队列为空。
  3. 返回第一步。

代码分析

function f1() {
  console.log("f1");
}

function f2() {
  console.log("f2");
}

function f3() {
  console.log("f3");
}

function f4() {
  const s = new Date().getSeconds();

  setTimeout(function f5() {
    console.log("f5");
  }, 500);

  while (true) {
    if (new Date().getSeconds() - s >= 2) {
      console.log("looped for 2 seconds");
      break;
    }
  }
}

setTimeout(f1, 0);

Promise.resolve().then(f2).then(f3);

new Promise((resolve) => {
  console.log("global");
  resolve();
}).then(f4);

如果读过我的从 ES6 规范解读执行上下文,那么理解起来更加深刻,当然没有这方面知识也不妨碍继续读下去。

一步一步分析:

全局上下文入栈,执行全局代码。

声明函数。

setTimeout,设置一个定时器,当定时器触发时(事实上,定时有一个最小值,所以设置时间为 0 的时候,会以定时最小值触发),会把回调函数 f1 推入宏任务队列。

Promise.resolve(),将第一个 then 中的回调函数 f2 放入微任务队列。

new Promise,执行代码,打印 global。将 then 中的回调函数 f4 放入微任务队列。

全局代码执行完毕。

此时事件循环模型如下:

宏任务队列为空。

微任务队列不为空,推出 f2 放入调用栈,执行 f2 后调用栈弹出 f2,注意:执行完 f2 后,f3 又被放入微任务队列。打印 f2。

微任务队列不为空,推出 f4 放入调用栈,执行 f4 后调用栈弹出 f4,注意:在 f4 中添加了一个定时器,而 while 循环执行了 2s,所以程序里的俩定时器都已经触发了,先推入 f1,再推入 f5 进入宏任务队列。打印 looped for 2 seconds。

微任务队列不为空,推出 f3 放入调用栈,执行 f3 后调用栈弹出 f3。打印 f3。

微任务队列为空。

宏任务队列不为空,推出 f1 放入调用栈,执行 f1 后调用栈弹出 f1。打印 f1。

微任务队列为空。

宏任务队列不为空,推出 f5 放入调用栈,执行 f5 后调用栈弹出 f5。打印 f5。

微任务队列为空。

宏任务队列为空。