什么是事件循环,这是一道面试官会经常问的题目。
尝试口语化回答一下:
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 运行时环境
总结:
- 任务队列中是宏任务,宏任务由宿主(host)发起。
- 每个宏任务中可能包含微任务,微任务是由JavaScript引擎发起,是Promise回调的底层实现。
- 执行顺序:【宏任务->微任务队列->浏览器绘制渲染】(事件循环1)=>【宏任务->微任务队列->浏览器绘制渲染】(事件循环2)=>...
- 用户界面绘制渲染、响应用户事件和事件循环,都在主线程中。
以上总结,若有不妥之处,还请大家指出,谢谢。