前言
在讨论事件循环之前,其实是想介绍 V8 引擎与浏览器之间的联系,奈何学艺不精,网上的资料也是参差不齐,所以不敢侃大山。
不过还是可以确定以下几点:
- 浏览器提供了 JS 代码运行的额外环境,如 window 对象、事件循环模型。
- V8 引擎只负责执行 JS 代码,即调用栈中有啥就执行啥,至于代码什么时候、什么顺序入栈由浏览器控制。
事件循环的作用
众所周知,JavaScript 是单线程运行的,这就意味着执行像 setTimeout 中的回调函数,如果没有一个机制让代码继续执行,那么 JS 程序只能阻塞等待函数执行。
Event Loop(事件循环)就是用来解决这问题,这同样也是 JS 实现异步的原因。
阻塞/非阻塞 同步/异步
既然提到了这个概念,作者这里再次声明:专业术语的概念往往不是绝对的,特别是当其应用在广泛的领域。
对于计算机语言领域,阻塞/非阻塞往往等同于同步/异步,最常用于 IO 操作,如对文件进行写入操作。如果是阻塞式的,那么就会等待写入完成才能继续后面的代码。如果是非阻塞的,会继续执行代码,只不过在写入结束时会产生相应信号来告知写入完成。
事件循环的组成
事件循环模型由 3 个部分组成:调用栈、宏任务队列、微任务队列。
栈和队列都是常见的数据结构,这里不多做介绍。
对于浏览器来说 MacroTask(宏任务)主要包括 setTimeout、setInterval、requestAnimationFrame,MicroTask(微任务)主要包括 Promise。
事件循环的进程模型:
- 判断宏任务队列是否为空,如果不为空,推出任务放入调用栈,执行代码。
- 判断微任务队列是否为空,如果不为空,推出任务放入调用栈,执行代码,再次重复该步骤,直到微任务队列为空。
- 返回第一步。
代码分析
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。
微任务队列为空。
宏任务队列为空。