前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
在ECMAScript 里面经常提到几个概念:
- Realm
- Agent
- Agent Cluster
但是相关的资料缺非常的少,这几个都是描述执行环境和资源管理的关键抽象概念。从作用范围,可以排个序
Agent Cluster > Agent > Realm。
简而言之(浏览器宿主环境)
- Realm 提供了代码运行最基本的资源(代码,内置对象,全局环境记录等),确保了不同网页或iframe之间的安全性
- Agent和Agent Cluster则反映了JavaScript执行背后更为复杂的资源管理和调度机制
先看一个整体的结构图

注意:
- 协议中是有提到,执行线程是可以被多个Agent共享的,本文不会过多讨论。出现这种情况的场景之一就是操作系统资源相当匮乏,比如1核0.5G。
Realm 领域
所有 ECMAScript 代码必须与一个领域相关联。从概念上讲,领域由
- 一组内部(内置)对象(开发者常见的Array,Object, Math等)、
- 一个 ECMAScript 全局环境、
- 该全局环境范围内加载的所有 ECMAScript 代码
- 以及其他相关的状态和资源组成。
Realm Record 字段
字段名 | 值 | 含义 |
---|---|---|
[[AgentSignifier]] | Agent唯一标志符 | realm关联的Agent |
[[Intrinsics]] | Record | 与此领域关联的代码使用的内部值(内部对象) |
[[GlobalObject]] | Object 或者 undefined | 全局对象 |
[[GlobalEnv]] | Global Environment Record | 全局环境记录 |
[[TemplateMap]] | ?? | ?? |
[[LoadedModules]] | Module Record | 模块记录以及相关信息 |
[[HostDefined]] | 任何值 | 为宿主保留的字段,宿主自定义的额外信息。 |
这意味着不同Realm之间的全局对象和内置函数是隔离的。在浏览器环境中, 通常一个 Window 对象对应着一个Realm。比如一个页面内嵌一个同源的页面,但是两个页面的Array对象却是不同的。
javascript
javascript
复制代码
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const xArray = window.frames[window.frames.length - 1].Array;
// 不同领域的Array不相等
xArray === Array // false
const arr = new xArray(1, 2, 3); // [1, 2, 3]
// Array.isArray方法是跨领域的
Array.isArray(arr); // true
// instanceof
arr instanceof Array; // false
下面的场景都会看到Realm的身影:
- 获取全局对象
- 获取全局环境记录
- 查找内置对象
Agent 代理
Agent (代理)包括
- 一组 ECMAScript 执行上下文、
- 一个执行上下文堆栈、
- 一个正在运行的执行上下文、
- 一个Agent记录(Agent Record)
- 一个执行线程(即JavaScript代码被解析和执行的线程)。
除了执行线程可能被多个Agent共享之外,Agent的所有组成部分都是该Agent独有的。
一个Agent可以通过其执行线程来处理多个执行上下文,但这始终是在单线程的基础上进行的,即任何时候只有一个执行上下文在执行。
举个场景: 一个执行线程可能负责浏览器标签A, B的代码执行,这种情况下,标签页A代码被执行时,标签页B的代码不会被执行的,UI就不会得到更新。
实际运行中,当JavaScript引擎开始执行一段代码时,会为其创建执行上下文,并将执行上下文压入执行上下文栈中。执行引擎通过执行上下文栈来切换和管理不同的执行上下文,确保代码按预期顺序执行。
而Agent作为更高级别的抽象概念,负责管理这些执行上下文,包括但不限于分配执行线程资源,协调执行上下文的切换与执行。
所以,尽管执行上下文不能单独运行,但它是代码得以执行的基础单元,必须在Agent的统筹下结合执行线程才能完成代码的实际运行。
执行线程被多个Agent共享
详情参见:9.7 Agents

有些网络浏览器会在浏览器窗口的多个不相关标签(非同源等) 页之间共享单个执行线程。例如,这意呀着这些标签页尽管彼此独立,但在同一浏览器实例中可能共用资源和执行环境,从而可能影响彼此的性能表现和资源使用。这种设计可以减少资源消耗,但也可能导致一个标签页中的耗时操作影响到其他标签页的响应速度。
现代浏览器浏览器一般不会这样,下面场景可能会出现
- 操作系统资源相当匮乏,比如1核0.5G
- 当浏览器打开了太多的窗口
这么做的本质是为了节约资源,本章节的示例均不考虑该种情况。
Agent 分类
ECAMScript 协议本身没有对Agent做太多说明,但是可以在 HTML标准 8.1.2 Agents and agent clusters 章节可以看到多种Agent类型的定义:
-
包含各种 Window 对象,这些对象可以 直接 或通过使用 document.domain 相互联系 。 这里强调是可以直接访问, 比如下例子:
如果a.html和b.html域名完全一样, a.html所在的Window实例是可以访问 b.html所在的Window实例,并且可以进行方法调用。 既然是可以直接调用,那自然也就是共享执行线程了。
xmlhtml 复制代码 // a.html <iframe src="./b.hmtl"></script> <script> frames[0].console.log(("say something") </script>
相似来源 (Similar-origin) window agent 这个怎么解释呢?
如果 页面
https://example.com/
用Iframe内嵌了 页面https://sub.example.com/
,通常情况下,当两个文档都运行了代码document.domain = "example.com"
之后,两个文档就能够同步地运行对方的脚本。javascripthtml 复制代码 // https://example.com/a.html <iframe src="https://sub.example.com/" id="sub"></iframe> <script> window.frames[0].console.log("messsage from sub.example.com"); </script> // https://sub.example.com/b.html <script> document.domain = "example.com"; </script> <div>sub.example.com</div>
host 因为一个是
example.com
一个是sub.example.com
,所以 是 Similar-origin(自行体会),有别于同源。需要注意的是, chrome自 112版本开始,就禁止设置
document.domain
了,具体的可以参见chrome的官方文档 Chrome 将禁止修改 document.domain 以放宽同源政策。 -
Dedicated worker agent
专有worker Agent。程序上对应 Worker 对象。
通常情况个,每个 Worker 实例就是一个Agent, 正常情况下的其执行线程也不会被共享。
-
Shared worker agent
共享worker Agent。 对应编程中的 SharedWorker 对象,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker, 是跨窗口通讯(限同源)的一种手段。
如下演示的就是通过ShareWoker进行一个窗口发消息,多窗口收消息的场景, 在线演示地址 ShareWorker多窗口通讯。
-
Service worker agent
PWA 中重要的组成部分。程序上对应 ServiceWorker 对象。 每个都是独立的Agent。
-
Worklet agent
Worklet 是 Workers 的轻量级版本,它允许在浏览器的后台线程中执行特定的任务,主要用于处理与渲染和动画相关的计算密集型任务。Worklet 主要有两种类型:CSS Worklet 和 Web Animations Worklet(即 AnimationWorklet),以及 AudioWorklet。
Agent, Realm,Window 的关系
-
一个Window对应的有一个Realm, Realm有一个
[[GlobalThis]]
属性指向 Window的实例,每个Window的下的内置对象是不同的。inijavascript 复制代码 const iframe = document.createElement("iframe"); document.body.appendChild(iframe); const xArray = window.frames[window.frames.length - 1].Array; // 不同领域的Array不相等 xArray === Array // false
-
一个Agent 对应有一个 事件循环的线程,一个事件循环可以涉及到多个Realm (Window实例)。 即多个Window实例共享一个事件循环。
通常情况下,同源下的
index.html
和iframe.html
共用一个事件循环。javascripthtml 复制代码 // index.html <iframe src="./iframe.html"></iframe> <script> function checkExecution() { console.log('Checking execution in index page', new Date().toTimeString()); } setInterval(checkExecution, 1000); // 每秒执行一次 </script> // iframe.html <script> function longRunningOperation() { const startTime = Date.now(); let i = 0; while (Date.now() - startTime < 5000) { // 假设执行5秒 i++; } console.log('Long operation finished in iframe page', new Date().toTimeString()); } setTimeout(() => { longRunningOperation(); }, 2000) </script>
- index.html 前2000ms每秒输出,
- 2000ms之后因为iframe.html的阻塞,导致了 index.html暂停了输出,
- 5000ms之后,又继续输出。

-
同源的Window实例可能处于不同Agent,如果没有直接关联。比如
- 直接地址栏打开两个页面
rel=noopener
打开的新页面
怎么去验证呢,很简单呢,每个Agent有自己独立的执行线程。使用
rel=noopener
打开的other.html, other.html的阻塞不影响index.html的输出即可。 这里的 index.html和other.html就属于不同的Agent。javascripthtml 复制代码 // index.html <h1> 同源下的不个Realm(Window)拥有独自的Agent</h1> <a href="./other.html" target="_blank" rel="noopener">other页面</a> <script> function checkExecution() { console.log('Checking execution in index page', new Date().toTimeString()); } setInterval(checkExecution, 1000); // 每秒执行一次 </script> // other.html <script> // 页面1(或Web Worker1) function longRunningOperation() { console.log('Long operation started ', new Date().toTimeString()); const startTime = Date.now(); let i = 0; while (Date.now() - startTime < 5000) { // 假设执行5秒 i++; } console.log('Long operation finished ', new Date().toTimeString()); } setTimeout(() => { longRunningOperation(); }, 2000) </script>
看输出结果
修改
rel="opener"
,结果会怎么样呢?
Agent Clusters 代理集群
代理集群是可以通过操作共享内存进行通信的代理集合。比如可以通过 SharedArrayBuffer 等内置对象进行内存共享。
虽然 SharedArrayBuffer 也需要通过 postMessage
在不通的 Agent 之间传递,但是之后数据的变化却是会同步的,当然协议定义了Atomics Object 定义了 atomic operation(原子操作) 来修改数据和监听数据变化。
这里不展开了,你知道 SharedArrayBuffer 是代理群不同Agent(代理)之间共享数据的手段就行。
在同一代理集群场景
拿浏览器宿主环境来说,html协议中给出了agent cluster 示例, 可以加强理解。
以下每一对全局对象都属于相同的代理集群(agent cluster),因此它们之间能够使用SharedArrayBuffer实例来共享内存:
-
Window 对象和它创建的专用 dedicated worker(对应编程中的 Worker 对象)。
如下
a.html
窗口和worker.js
的全局对象,在同一个代理集群。xmlhtml 复制代码 // a.html <script> var worker = new Worker("./worker.js") </script>
-
一个 worker (任何类型) 和 她创建的 dedicated worker。
如下
worker.js
和subworker.js
的全局对象javascripthtml 复制代码 // a.html <script> var worker = new Worker("./worker.js") </script> // worker.js var subWorker = new Worker("./subworker.js"); // subworker.js console.log("subworker is working");
-
窗口A,以及窗口创建的 iframe (same origin-domain)
csshtml 复制代码 // a.html <iframe src="./b.html" ></iframe> // b.html <div>i am b</div>
-
一个 Window 对象和打开它的Window (same origin-domain)。
csshtml 复制代码 // a.html <a href="./b.html" target="_blank">页面B</a> // b.html <div>i am b</div>
-
一个 Window 对象和它创建的一个 worklet。
Worklet 接口是 Web Workers 的轻量级版本,允许开发人员访问呈现管道的底层部分。不深入。
不在同一代理集群场景
下面的全局对象不在一个代理集群,不能共享内存。
-
一个 Window 对象和它创建的共享工作线程 shared worker 。
shared worker 可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker, 是跨窗口通讯(限同源)的一种手段。
xmlhtml 复制代码 // a.html <script> var worker = new SharedWorker("./worker.js") </script>
-
一个 worker (任何类型) 和 她创建的 shared worker 。
javascripthtml 复制代码 // a.html <script> var worker = new Worker("./worker.js") </script> // worker.js var subWorker = new SharedWorker("./subworker.js"); // subworker.js console.log("subworker is working");
-
Window 对象及其创建的 ServiceWorker。
cjavascript 复制代码 // a.html <script> navigator.serviceWorker .register("service-worker.js", { scope: "./", }) </script> // service-worker.js console.log("service-worker is working...");
-
一个 Window 对象和一个 iframe 元素的 Window 对象(不同源)。
lessjavascript 复制代码 // a.html www.a.com <iframe src="https:/www.b.com/b.html" ></iframe> // b.html www.b.com 与 a.html不同源 <div>i am b</div>
-
任意两个没有opener或祖先关系的 Window 对象。即使这两个 Window 对象同源。
正常情况,Window A使用
a
标签或者window.open
方法打开的新Window B是有关联的, B的opener
指向的是 Window A。但是这个是可以改变的。比如
a
可以设置rel="noopener"
, 新打开的Window B的 opener 将变为空。csshtml 复制代码 // a.html <a href="./b.html" target="_blank" rel="noopener">页面B</a>
别小看这个 rel="noopener"
属性,如果Window A 是资源消耗很高的页面, 在条件允许的情况, 新开的窗口 Window B 会单开线程,保证 a.html 所在窗口 Window A的流畅性。
再谈关系
关于Agent 和 Realm , 协议的 一个issue What is an Agent 也许帮你更好的理解:
- Realm :主要关注全局对象集和全局执行环境的隔离。每个Realm拥有自己独立的全局对象、内置对象、全局变量和函数空间。在浏览器环境中,每个窗口、iframe或Service Worker等通常对应一个独立的Realm,它们之间的全局对象和作用域是隔离的。
- Agent :关注的是并发执行和执行上下文的管理。Agent管理一组执行上下文,并且可以拥有自己的执行线程、执行上下文栈和作业队列。一个Agent可以包含任意数量的Realm,而每个Realm必须隶属于一个Agent。
- Origin:在浏览器安全模型中,Origin表示同源策略的单位,它跨越Agent,即一个Origin可以包含多个运行在不同Agent中的Realms。在浏览器中,来自同一Origin的脚本可以在不同的窗口或iframe之间共享数据,但受到同源策略的约束。
- Agent Clusters(代理集群):一组Agent, Agent之间可以通过 SharedArrayBuffer 共享内存。
再推导:
- 作业队列是针对每个Agent(代理)的,而不是针对每个Realm(领域)的。
- Agent 包含了所有可能在同一个事件循环(所有 similar-origin"相似起源"的 Window 对象)中执行彼此的函数的Realm(领域)。
简单案例
如代码,有这几个额,就叫做Realm吧,自行脑补哈:
- index.html
- other.html
- iframe.html
- worker.js
index.html
被打开后,会立刻打开other.html
javascript
javascript
复制代码
// index.html
<iframe src="./iframe.html"></iframe>
<script>
function checkExecution() {
console.log('Checking execution in index page', new Date().toTimeString());
}
setInterval(checkExecution, 1000); // 每秒执行一次
window.open("./other.html")
</script>
<script>
new Worker("./worker.js")
</script>
// iframe.html
<script>
function longRunningOperation() {
console.log('Long operation started ', new Date().toTimeString());
const startTime = Date.now();
let i = 0;
while (Date.now() - startTime < 5000) { // 假设执行5秒
i++;
}
console.log('Long operation finished ', new Date().toTimeString());
}
// 2000ms后,阻塞5000ms
setTimeout(() => {
longRunningOperation();
}, 2000)
</script>
// other.html
<script>
function longRunningOperation() {
console.log('other: Long operation started ', new Date().toTimeString());
const startTime = Date.now();
let i = 0;
while (Date.now() - startTime < 5000) { // 假设执行5秒
i++;
}
console.log('other:Long operation finished ', new Date().toTimeString());
}
// 5000ms后,阻塞5000ms
setTimeout(() => {
longRunningOperation();
}, 5000)
</script>
// worker.js
function longRunningOperation() {
console.log('worker:Long operation started ', new Date().toTimeString());
const startTime = Date.now();
let i = 0;
while (Date.now() - startTime < 5000) { // 假设执行5秒
i++;
}
console.log('worker:Long operation finished ', new Date().toTimeString());
}
setTimeout(() => {
longRunningOperation();
}, 2000)
function checkExecution() {
console.log('worker: Checking execution', new Date().toTimeString());
}
setInterval(checkExecution, 1000); // 每秒执行一次
请问,
- 上面有几个Agent和 Agent Cluster
- index.html的输出情况
答案
- 一个Agent Cluster , 两个Agent

- iframe.html和other.html 的阻塞会影响index.html的输出,而worker.js 这就自己玩自己的。

探讨
- iframe.html 2000ms后阻塞, other.html 5000ms后阻塞,为什么index.html的阻塞时间是 10*1000ms
- 如果把上述index.html的代码调整为下面,两个问题的答案又如何呢
xml
javascript
复制代码
// index.html
<iframe src="./iframe.html"></iframe>
<script>
function checkExecution() {
console.log('Checking execution in index page', new Date().toTimeString());
}
setInterval(checkExecution, 1000); // 每秒执行一次
window.open(url, '_blank', 'noopener=yes')
</script>
<script>
new Worker("./worker.js")
</script>