在 Event Loop 之前会发生什么
Source: Dev.to
JavaScript 代码并不是在任务出现在调用栈的那一刻就被引擎本地执行的。大多数文章只关注事件循环(Event Loop)的工作原理,而忽略了开发者编写的指令在到达那一步之前所走的路径。因此,作用域、闭包、暂时性死区(Temporal Dead Zone)以及其他执行细节常常被记忆为一种“技术魔法”。
在这系列短文中,我们追踪从源码输入到实际执行的全过程。为了避免过于深入特定引擎(V8、SpiderMonkey、JavaScriptCore 等)的实现细节,讨论停留在 ECMAScript 262 规范所定义的抽象层次上。
规范有意 不 将 JavaScript 执行描述为线性的步骤序列——实现细节留给引擎作者自行决定。尽管如此,结合规范本身以及对引擎实现的实际了解,我们仍然可以辨识出四个有条件的阶段:
- 确定执行环境:Host、Agent、Realm
- 解析源代码
- 创建元对象:模块记录(Module Record)/ 脚本记录(Script Record)
- 程序执行
执行准备阶段
解析(Parsing)和初始化(initialisation)在此进行。引擎会创建必要的内部记录(例如 ScriptRecord、ModuleRecord),并在任何用户代码运行之前搭建全局环境。
指令执行阶段
引擎遍历已编译的字节码 / 抽象语法树,创建执行上下文(execution contexts),求值表达式,最终执行程序定义的副作用。
执行环境层
规范定义了三个关键层次的执行环境,这些层次必须在任何开发者指令运行之前就已经存在。
Host
执行 JavaScript 代码的外部程序:浏览器、Node.js、Deno 等。
Agent
执行环境的一个隔离部分(独立于其他 Agent),拥有自己的执行上下文栈(Execution Context Stack)和正在运行的执行上下文(Running Execution Context)。例子包括浏览器标签页、Web Worker 或 iframe。
Realm
一个逻辑实体,定义全局对象、全局方法和全局变量,并负责创建全局执行上下文(Global Execution Context)。在 V8 引擎实现中,这对应于 C++ 中的 v8::Context 类:
// Example: creating a new V8 context
v8::Isolate* isolate = ...;
v8::Local context = v8::Context::New(isolate);
理解执行环境的结构及其隔离模型可以解释:
- 同一浏览器中不同标签页之间不存在竞争条件。
- 为什么 Worker 无法访问主应用的全局对象和变量。
- 单一 Host 内部被覆盖的全局 JavaScript 方法的隔离性。