js事件循环EventLoop总结

308 阅读5分钟

什么是事件循环,这是一道面试官会经常问的题目。

尝试口语化回答一下:

JavaScript引擎不断地从一个任务(消息)队列中获取任务,并执行该任务后,再次获取任务执行,这样的实现机制叫做事件循环(EventLoop)。

MDN:并发模型与事件循环

文中,解释了这一概念。 执行栈:文中讲了函数调用的执行过程。比如:递归调用时,要注意结束条件,不然会出现栈溢出。

事件循环伪代码

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

在一次事件循环中,当前执行栈完全清空后,才会执行下一次获取任务。这里消息队列中的任务就是宏任务Task。

宏任务Task在什么时候被添加呢?

在 JavaScript 中通过 queueMicrotask() 使用微任务文中给出了:

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

描述的很详细,dom事件回调函数,setTimeout等都会添加宏任务。 通过以上,所以setTimeout(fn,0)的含义,并不是0s后立即执行。添加了一个宏任务,如果之前任务还未执行完成,要等待执行完成,要排队按顺序执行。所以JavaScript引擎同一时刻只执行一个任务,所以经常说JavaScript是单线程的,体会一下。不用处理多线程下资源共享加锁的问题(像C#中提到的是否线程安全),所以写JavaScript不用关心线程安全的问题。

如果需要执行密集CPU的任务,可以放在web worker中,web worker是开启另一个新线程,不会影响JavaScript主线程,主线程还需要响应用户事件、浏览器重排重绘等。

微任务(# Microtasks)

微任务的执行时机是在宏任务执行之后执行,微任务也有微任务队列,等所有微任务都执行完毕后,才会进入下次事件循环。通过

queueMicrotask

添加一个微任务。文中提到之前通过下面技巧

Promise.resolve().then(() => {
    //microTask, 不推荐有缺点
})

也可添加一个微任务,但有缺点,引用文中

通过引入 queueMicrotask(),由晦涩地使用 promise 去创建微任务而带来的风险就可以被避免了。举例来说,当使用 promise 创建微任务时,由回调抛出的异常被报告为 rejected promises 而不是标准异常。同时,创建和销毁 promise 带来了事件和内存方面的额外开销,这是正确入列微任务的函数应该避免的。

何时需要使用微任务呢?

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

1、保证条件性使用 promises 时的顺序

具体看原文中例子

2、批量操作

也可以使用微任务从不同来源将多个请求收集到单一的批处理中,从而避免对处理同类工作的多次调用可能造成的开销。

const messageQueue = [];

let sendMessage = (message) => {
  messageQueue.push(message);

  if (messageQueue.length === 1) {
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;
      fetch("url-of-receiver", json);
    });
  }
};

当 sendMessage()

被调用时,指定的消息首先被推入消息队列数组。接着事情就变得有趣了。

如果我们刚加入数组的消息是第一条,就入列一个将会发送一个批处理的微任务。照旧,当 JavaScript 执行路径到达顶层,恰在运行回调之前,那个微任务将会执行。这意味着之后的间歇期内造成的对 sendMessage() 的任何调用都会将其各自的消息推入消息队列,但囿于入列微任务逻辑之前的数组长度检查,不会有新的微任务入列。

当微任务运行之时,等待它处理的可能是一个有若干条消息的数组。微任务函数先是通过 JSON.stringify() 方法将消息数组编码为 JSON。其后,数组中的内容就不再需要了,所以清空 messageQueue 数组。最后,使用 fetch() 方法将编码后的 JSON 发往服务器。

这使得同一次事件循环迭代期间发生的每次 sendMessage() 调用将其消息添加到同一个 fetch() 操作中,而不会让诸如 timeouts 等其他可能的定时任务推迟传递。

服务器将接到 JSON 字符串,然后大概会将其解码并处理其从结果数组中找到的消息。

如果代码的其他位置多次调用sendMessage(),比如5次,那么在宏任务代码执行完成后,执行微任务,通过一次http请求,把数据传给后端。如果用setTimeout(fn,0)来做,要等到该宏任务执行,明显会晚于例子中实现,大家可以体会一下两者的执行时机差别。

Promise

任务队列_vs._微任务

Promise 回调被处理为微任务,而 setTimeout() 回调被处理为任务队列。

深入:微任务与 Javascript 运行时环境

总结:

  1. 任务队列中是宏任务,宏任务由宿主(host)发起。
  2. 每个宏任务中可能包含微任务,微任务是由JavaScript引擎发起,是Promise回调的底层实现。
  3. 执行顺序:【宏任务->微任务队列->浏览器绘制渲染】(事件循环1)=>【宏任务->微任务队列->浏览器绘制渲染】(事件循环2)=>...
  4. 用户界面绘制渲染、响应用户事件和事件循环,都在主线程中。

以上总结,若有不妥之处,还请大家指出,谢谢。