浏览器 Event Loop
在讲解浏览器的 Event Loop 前,我们需要先了解一下 JavaScript 的运行机制:
- 所有的任务分为同步任务和异步任务
- 所有同步任务都在主线程上执行,形成一个 “执行栈”(execution context stack)。
- 主线程之外,存在一个 “任务队列”(task queue),在走主流程的时候,如果碰到异步任务,那么就在 “任务队列” 中放置这个异步任务,如果异步任务有了运行结果,就在任务队列中注册事件。
- 一旦 “执行栈” 中所有同步任务执行完毕,系统就会读取 “任务队列”,看看里面存在哪些事件。那些对应的异步任务,结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面三个步骤。
- 一般情况下,宏任务先于微任务执行。
而 JavaScript 的异步任务,还细分两种任务:
- 宏任务(Macrotask):
script(整体代码)、setTimeout、setInterval、XMLHttpRequest.prototype.onload、I/O、UI 渲染 - 微任务(Microtask):
Promise、MutationObserver
示例 1
// 位置 1setTimeout(function () {console.log('timeout1');}, 1000);// 位置 2console.log('start');// 位置 3Promise.resolve().then(function () {// 位置 5console.log('promise1');// 位置 6Promise.resolve().then(function () {console.log('promise2');});// 位置 7setTimeout(function () {// 位置 8Promise.resolve().then(function () {console.log('promise3');});// 位置 9console.log('timeout2')}, 0);});// 位置 4console.log('done');
提问:请指出上面代码的输出结果?
回答:
这是经典的面试题型,所以咱们看到不用慌,先拿我们上面的点,区分下分宏任务和微任务:
- 宏任务(Macrotask):
script(整体代码)、setTimeout、setInterval、XMLHttpRequest.prototype.onload、I/O、UI 渲染 - 微任务(Microtask):
Promise、MutationObserver
OK,开始走流程:
- 首先碰到的是
script(整体代码),先看【位置 1】,属于宏任务setTimeout下的,所以做个标记,待会回来执行。 - 接着碰到【位置 2】,这是
script(整体代码)下的无阻碍代码,直接执行即可。 - 再来碰到【位置 3】,它现在是
script(整体代码)下的微任务,所以咱们做个标记,走完文件所有代码后,优先执行微任务,再执行宏任务。 - 最后碰到【位置 4】,它是
script(整体代码)下的无阻碍代码,直接执行即可。
这样,第一波步骤,我们输出的是【位置 2】的 start 和【位置 4】的 done。
我们接着走:
- 上面我们走完了第一遍代码,然后现在这一步先走
script(整体代码)下的微任务,即【位置 3】- 先碰到【位置 5】,这是无阻碍代码,直接执行。
- 再碰到【位置 6】,这是微任务,标记一下,等下执行完【位置 3】内所有代码后,优先执行它。
- 最后碰到【位置 7】,这是宏任务,丢入任务队列,看它和【位置 1】谁先走了。
- 走完一遍【位置 3】后,发现还有微任务【位置 6】,所以执行【位置 6】,进行打印输出。
到这一步,我们就走完了 script(整体代码)及之下的所有微任务了。
这时候,我们会说,【位置 1】和【位置 7】都被丢到任务队列了,是不是【位置 1】先走呢?
答案为:不是的。
同样的 setTimeout,我在测试的时候,就发现它们的输出结果在各个环境都有自己的流程,有时候先走【位置 7】,再走【位置 1】;而有时候先走【位置 1】,再走【位置 7】。
当然,如果你指定是在 Chrome 的控制台输出一下上面的代码,那就是先【位置 7】,再【位置 1】~
- point:不要主观臆断某个代码会怎么走,最好还是直接实况运行走一波!
- 先走【位置 7】。碰到【位置 8】,将其添加到【位置 7】的微任务中,等【位置 7】所有代码执行完毕回来优先走微任务;碰到【位置 9】,这是无阻碍代码,直接输出即可。
- 执行【位置 7】的微任务【位置 8】,输出对应文本。
- 最后走【位置 1】,输出对应文本。
所以答案是:
startdonepromise1promise2timeout2promise3timeout1
示例 2
在上面,jsliang 花费了许多口水,讲了一些繁杂冗余的步骤,所以下面这个示例,请小伙伴们先自行猜设,得出结论后再翻看答案和调试 GIF~
示例 2
console.log("script start");setTimeout(function() {console.log("setTimeout---0");}, 0);setTimeout(function() {console.log("setTimeout---200");setTimeout(function() {console.log("inner-setTimeout---0");});Promise.resolve().then(function() {console.log("promise5");});}, 200);Promise.resolve().then(function() {console.log("promise1");}).then(function() {console.log("promise2");});Promise.resolve().then(function() {console.log("promise3");});console.log("script end");
- 输出结果:
script startscript endpromise1promise3promise2setTimeout---0setTimeout---200promise5inner-setTimeout---0
示例 3
示例 3
setTimeout(function() {console.log(4);}, 0);const promise = new Promise(function executor(resolve) {console.log(1);for (var i = 0; i < 10000; i++) {i == 9999 && resolve();}console.log(2);}).then(function() {console.log(5);});console.log(3);
输出结果:
1 2 3 5 4如果不常用
Promise的小伙伴,可能对此感到疑惑,为啥不是:3 1 2 5 4?
手动滑稽,别问,问就是进一步探索Promise:《jsliang 的 Promise 探索》:github.com/LiangJunron…
当然,还没将所有探索结果更新,如果有小伙伴催更会加快速度,欢迎留言或者私聊催更,哈哈~
小结
这样,我们就通过 3 个示例,大致了解了浏览器的 Event Loop。
当然,实际应用中的代码,何止这么简单,甚至有时候,面试官给你的面试题,也会让你瞠目结舌。
所以,这里咱们废话两点:
- 你可以了解宏任务和微任务的大体执行,例如先走
if...else...,再走Promise……但是,详细到每个point都记下来,这里不推荐。大人,时代在进步,记住死的不如多在业务实践中尝试,取最新的知识。 - 浏览器的 Event Loop 和 Node.js 的 Event Loop 不同,万一哪天 XX 小程序搞另类,有自己的 Event Loop,你要一一记住吗?
Node.js Event Loop
那么,下面咱们吐槽下 Node.js 的 Event Loop。
说实话,看完 Node 官网和大佬们关于 Node.js 的 Event Loop 讲解,让我想起了 Vue、React、微信小程序 的【生命周期】,再联想到我们的人生仿佛就像被写死的程序一样周期性、事件性运行,非常可恶,哈哈~
上面我们讲解过:Node.js 的 Event Loop 是基于 libuv。libuv 已经对 Event Loop 作出了实现。
那么其机制是怎样子的呢?看图:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
关于这 6 个阶段,官网描述为:
- 定时器(timers):本阶段执行已经被
setTimeout()和setInterval()的调度回调函数。 - 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()调度的之外),其余情况Node将在适当的时候在此阻塞。 - 检测(check):
setImmediate()回调函数在这里执行。 - 关闭的回调函数(close callbacks):一些关闭的回调函数,如:
socket.on('close', ...)。
当然,这里 jsliang 并不想画蛇添足,将官网或者其他大佬的文章照搬过来说是自己的,推荐小伙伴们阅读官网关于 Event Loop 的各个阶段的描述,以期在工作中有所使用
Node.js v9.5.0 Event Loop
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
但是,迫于生活所需,有些时候,前端面试官还是会跟你扯 setTimeout & setImmediate 和 process.nextTice()。
1 setTimeout & setImmediate
- setTimeout:众所周知,这是一个定时器,指定
n毫秒后执行定时器里面的内容。 - setImmediate:Node.js 发现使用
setTimeout和setInterval有些小弊端,所以设计了个setImmediate,该方法被设计为一旦在当前轮询阶段完成,就执行这个脚本。
当然,光说无益,看代码:
index.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
猜测下在 VS Code 中执行 node index.js 命令会发生什么?
结局 1
immediate
timeout
结局 2
timeout
immediate
事实上这两个结局都是会存在的,看似 happy ending,但是有的小伙伴可能心里闹翻天。
按照官网的解释:
- 执行计时器的顺序将根据调用它们的上下文而异。
- 如果两则都从主模块内调用,则计时器将受到进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
如果你将这两个函数放入一个 I/O 循环内调用,
setImmediate总是被有限调用。const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); 复制代码虽然官方解释的很 巧妙,但是不管你懂不懂,反正我觉得有点扯淡。
最后再来句官方总结:使用
setImmediate()相对于setTimeout的主要优势是:如果setImmediate()是在 I/O 周期内被调度的,那么它将会在任何的定时器之前执行,跟这里存在多少个定时器无关。
enm…后面如果我具体使用 Node.js 的时候,我再进一步观察吧,至于现在,我还是先了解下即可。
2 process.nextTick()
nextTick 比较特殊,它存有自己的队列。
并且,它独立于 Event Loop,无论 Event Loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。
还有需要注意的是:process.nextTick() 优先于其他的微任务(microtask)执行。
当然,如果你对此有所兴趣,你可以进一步探索源码,或者观察大佬们探索源码:
没有使用就没有发言权,作为一个 Node.js 菜鸡,这里就不妄加评论分析了。
3 示例 1
下面开始示例,我们看下 Node.js 的 Event Loop 有何差异:
示例 1
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
});
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function() {
console.log("promise2");
});
});
如果你还记得上面讲解的浏览器的 Event Loop,你可能会将答案直接写成:
浏览器 Event Loop 输出:
timer1
promise1
timer2
promise2
是的你是对的,那就是浏览器的 Event Loop,到了 Node.js 这块,就有不同变化了:
Node.js Event Loop 输出:
timer1
timer2
promise1
promise2
尝试接受它!
然后大声默念:根据具体环境进行对应观察和得出结论。
4 示例 2
下面咱们再看一个示例:
示例 2
setTimeout(function () {
console.log(1);
});
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log('end');
node index.js
2
4
end
3
5
1
6
这里不打算解析,因为我怕初识 Event Loop 的小伙伴看完解释后懵逼,然后搞混淆了。
实话:我也不敢解析,因为我就是 Node.js 菜鸡
5 小结
终上所述,我们进行小结:
Node 端事件循环中的异步队列也是这两种:Macrotask(宏任务)队列和 Microtask(微任务)队列。
- 常见的 Macrotask:
setTimeout、setInterval、setImmediate、script(整体代码)、 I/O 操作等。 - 常见的 Microtask:
process.nextTick、new Promise().then(回调)等。
参考文献
- 《Tasks, microtasks, queues and schedules》 - Jake
- 《彻底搞懂浏览器 Event-loop》 - 刘小夕
- 《彻底理解 JS Event Loop(浏览器环境)》 - 93
- 《彻底弄懂浏览器端的 Event-Loop》 - 长可
- 《什么是浏览器的事件循环(Event Loop)?》 - 鱼子酱
- 《理解event loop(浏览器环境与nodejs环境)》 - sugerpocket
- 《从 event loop 规范探究 JavaScript 异步及浏览器更新渲染时机》 - 杨敬卓
- 《跟着 Event loop 规范理解浏览器中的异步机制》 - fi3ework
- 《不要混淆 nodejs 和浏览器中的 event loop》 - youth7
- 《浏览器的 event loop 和 node 的 event loop》 - 金大光
- 《浏览器与 Node 的事件循环(Event Loop)有何区别?》 - 浪里行舟
- 《浏览器和 Node 不同的事件循环(Event Loop)》 - toBeTheLight
- 《let 和 const 命令》 - 阮一峰
- 《Node.js Event Loop》 - Node.js 官网
