javascript 中的事件循环,阐明了代码的执行顺序。其中涉及了调用栈(call stack)
、消息队列(message queue)
、宏任务(macrotask/task)
、微任务(microtask/job)
这几个概念。
一. 可视化表示
下面这个图,是来自 MDN 的事件循环的可视化表示。
- 图中的 Stack 就是事件循环中的调用栈,它其中存放着一个一个的函数调用帧。每个函数调用后,就会创建一个调用帧放入栈中。每个帧都包含着对应函数的参数和局部变量。
- 图中的 Queue 是事件循环中的消息队列,它里面存放着一个一个的消息。这的消息,可以简单理解为一个个待执行的回调函数的引用。
- 当调用栈中的所有调用帧都执行完了,栈空的时候,事件循环就会从消息队列的队头里拿出一个消息,放入调用栈,执行它。
- 图中只划分出了一个简单的消息队列,其实还能细分为宏任务队列(task queue)和微任务队列(job queue)。
二. 伪代码表示
用一段伪代码表示 javascript 中的事件循环的话,应该是像下面这样的:
// 在循环中,不断轮询看调用栈中是否存在未执行的函数调用帧
while(true){
// 如果存在未执行的函数调用帧,则执行它
if(callStack.hasFrame()){
excute(frame);
} else {
// 如果不存在未执行的函数调用帧,则去消息队列拿出队头的消息,进入调用栈后执行它
let message = MessageQueue.getFirstMessage();
excute(message);
}
}
三. 事件循环过程阐述
- 事件循环检查调用栈里是否存在未执行的调用帧?如果有,执行它;执行完成后,将该调用帧出栈。
- 循环步骤1,直到调用栈为空。
- 检查微任务队列里是否存在排队中的微任务?如果有,则取出队列头的一个微任务,进入调用栈执行;执行完后,出栈。
- 循环步骤3,直到微任务队列为空。
- 检查宏任务队列里是否存在排队中的宏任务?如果有,则取出队列头的一个宏任务,进入调用栈执行;执行完后,出栈。
- 跳到步骤3,往下执行。
一句话概括一下,就是:调用栈里的调用帧执行完后,把微任务队列里的所有微任务执行完后,再执行一个宏任务;接下来依旧是执行依次所有微任务 --> 执行一个宏任务
这样的过程。
四. 举例类比理解
- 调用栈和消息队列 vs 手头的工作和待接的 api 接口:
前端在写着前端代码,写着写着,发现缺少一个 api。
这时候,前端去告诉后端提供一个 api。然后,前端继续写自己的代码去了。
不过,前端每过一段时间,就会去询问后端是否准备好 api 接口了。
直到后端回答说准备好了,前端才会停止询问。
这时候,前端把 api 拿过来放在代办事项中;等手头的代码写完以后,再去处理代办事项里的东西。
如果代办事项里只有这一个事项,那就立即接入这个 api 接口。
- 宏任务队列和微任务队列 vs 普通队列和 VIP 队列:
某天,小胖去玩跳跳床。
假设:
每次只能有一个人玩跳跳床;
现在有一个人在玩着;
此时有 10 个人在排着队等着玩。
这时候:
你要玩的话,就只能乖乖排队;
一开始在玩的人出来以后,队列头的 1 人进入玩耍;
等前面的 10 个人都玩了以后,才能轮到你进入玩耍。
假设:
每次只能有一个人在玩跳跳床;
现在有一个人在玩着;
此时有 10 个人排在普通队列;
有 1 个人排在 VIP 队列;
你是 VIP。
这时候:
你要玩的话,就到 VIP 队列排队;
当一开始在玩的人出来以后,VIP 队列头的 1 人进入玩耍;
等这个人玩耍结束以后,就轮到你进入玩耍了!
后续:
你结束玩耍出来以后,
- 如果 VIP 队列依旧有人,则还是 VIP 队列头的 1 人进入玩耍;
- 如果 VIP 队列没人了,则普通队列头的 1 人可以进入玩耍。