事件循环
JavaScript 具有基于 **事件循环** 的运行时模型,它负责执行代码、收集和处理事件以及执行排队的子任务。该模型与 C 和 Java 等其他语言中的模型截然不同。
运行时概念
以下部分解释了一个理论模型。现代 JavaScript 引擎实现了并对描述的语义进行了大量优化。
视觉表示
堆栈
函数调用形成一个 **帧** 堆栈。
function foo(b) {
const a = 10;
return a + b + 11;
}
function bar(x) {
const y = 3;
return foo(x * y);
}
const baz = bar(7); // assigns 42 to baz
操作顺序
- 调用
bar
时,将创建一个包含对bar
的参数和局部变量的引用的第一个帧。 - 当
bar
调用foo
时,将创建一个第二个帧并将其推送到第一个帧的顶部,其中包含对foo
的参数和局部变量的引用。 - 当
foo
返回时,将从堆栈中弹出顶部的帧元素(只剩下bar
的调用帧)。 - 当
bar
返回时,堆栈为空。
请注意,参数和局部变量可能仍然存在,因为它们存储在堆栈之外 - 因此它们可以被任何 嵌套函数 在其外部函数返回后很长时间内访问。
堆
对象在堆中分配,堆只是一个表示大块(大部分非结构化)内存区域的名称。
队列
JavaScript 运行时使用消息队列,消息队列是待处理消息的列表。每个消息都关联着一个用于处理该消息的函数。
在 **事件循环** 过程中的某个时刻,运行时开始处理队列中的消息,从最旧的消息开始。为此,将消息从队列中删除,并使用该消息作为输入参数调用其对应的函数。与往常一样,调用函数会为该函数使用创建一个新的堆栈帧。
函数处理持续进行,直到堆栈再次为空。然后,事件循环将处理队列中的下一条消息(如果有)。
事件循环
**事件循环** 由于其实现方式通常类似于以下内容而得名
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage()
异步等待消息到达(如果消息尚未到达且正在等待处理)。
"运行至完成"
每个消息在任何其他消息处理之前都完全处理。
这在推理程序时提供了一些很好的属性,包括以下事实:只要函数运行,它就不会被抢占,并且将在任何其他代码运行之前完全运行(并且可以修改函数操作的数据)。这与 C 不同,例如,如果函数在某个线程中运行,它可能在运行时系统运行另一个线程中的某些代码时被停止。
此模型的一个缺点是,如果消息处理时间过长,则 Web 应用程序将无法处理用户交互,如点击或滚动。浏览器通过 "脚本运行时间过长" 对话框来缓解这个问题。一个要遵循的良好做法是使消息处理简短,如果可能的话,将一条消息分成多条消息。
添加消息
在 Web 浏览器中,消息通常在发生事件并且事件监听器附加到该事件时添加。如果没有监听器,则事件将丢失。因此,点击具有点击事件处理程序的元素将添加一条消息 - 同样适用于任何其他事件。但是,有些事件会同步发生而无需消息 - 例如,通过 click
方法模拟点击。
函数 setTimeout
的前两个参数是待添加到队列的消息和时间值(可选;默认为 0
)。时间值 表示将消息推送到队列的(最小)延迟。如果没有其他消息在队列中,并且堆栈为空,则消息将在延迟后立即处理。但是,如果存在消息,则 setTimeout
消息将不得不等待其他消息被处理。因此,第二个参数表示最小时间 - 而不是保证时间。
以下示例演示了此概念(setTimeout
不会在计时器过期后立即运行)
const seconds = new Date().getTime() / 1000;
setTimeout(() => {
// prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
console.log(`Ran after ${new Date().getTime() / 1000 - seconds} seconds`);
}, 500);
while (true) {
if (new Date().getTime() / 1000 - seconds >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
零延迟
零延迟并不意味着回调将在零毫秒后触发。使用 0
(零)毫秒的延迟调用 setTimeout
不会在给定间隔后执行回调函数。
执行取决于队列中等待的任务数量。在下面的示例中,消息 "this is just a message"
将在回调中的消息被处理之前写入控制台,因为延迟是运行时处理请求所需的最小时间(而不是保证时间)。
setTimeout
需要等待所有排队消息的代码完成,即使你为 setTimeout
指定了特定的时间限制。
(() => {
console.log("this is the start");
setTimeout(() => {
console.log("Callback 1: this is a msg from call back");
}); // has a default time value of 0
console.log("this is just a message");
setTimeout(() => {
console.log("Callback 2: this is a msg from call back");
}, 0);
console.log("this is the end");
})();
// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"
多个运行时相互通信
Web 工作线程或跨域 iframe
具有自己的堆栈、堆和消息队列。两个不同的运行时只能通过使用 postMessage
方法发送消息来通信。该方法会将消息添加到另一个运行时,如果后者监听 message
事件。