ECMAScript中的Realm, Agent, Agent Clusters

前言

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类型的定义:

  • Similar-origin window agent

    包含各种 Window 对象,这些对象可以 直接通过使用 document.domain 相互联系这里强调是可以直接访问, 比如下例子:

    如果a.html和b.html域名完全一样, a.html所在的Window实例是可以访问 b.html所在的Window实例,并且可以进行方法调用。 既然是可以直接调用,那自然也就是共享执行线程了。

    xml 复制代码
    html
    复制代码
    // 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"之后,两个文档就能够同步地运行对方的脚本。

    javascript 复制代码
    html
    复制代码
    // 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 的关系

  1. 一个Window对应的有一个Realm, Realm有一个 [[GlobalThis]]属性指向 Window的实例,每个Window的下的内置对象是不同的。

    ini 复制代码
    javascript
    复制代码
    const iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    const xArray = window.frames[window.frames.length - 1].Array;
    
    // 不同领域的Array不相等
    xArray === Array                 // false
  2. 一个Agent 对应有一个 事件循环的线程,一个事件循环可以涉及到多个Realm (Window实例)。 即多个Window实例共享一个事件循环。

    通常情况下,同源下的index.htmliframe.html共用一个事件循环。

    javascript 复制代码
    html
    复制代码
    // 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之后,又继续输出。
  1. 同源的Window实例可能处于不同Agent,如果没有直接关联。比如

    1. 直接地址栏打开两个页面
    2. rel=noopener打开的新页面

    怎么去验证呢,很简单呢,每个Agent有自己独立的执行线程。使用rel=noopener打开的other.html, other.html的阻塞不影响index.html的输出即可。 这里的 index.html和other.html就属于不同的Agent。

    javascript 复制代码
    html
    复制代码
    // 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的全局对象,在同一个代理集群。

    xml 复制代码
    html
    复制代码
    // a.html
    <script>
      var worker = new Worker("./worker.js")
    </script>
  • 一个 worker (任何类型) 和 她创建的 dedicated worker

    如下worker.jssubworker.js的全局对象

    javascript 复制代码
    html
    复制代码
    // 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

    css 复制代码
    html
    复制代码
    // a.html
    <iframe src="./b.html" ></iframe>
    
    // b.html
    <div>i am b</div>
  • 一个 Window 对象和打开它的Window (same origin-domain)。

    css 复制代码
    html
    复制代码
    // 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, 是跨窗口通讯(限同源)的一种手段。

    xml 复制代码
    html
    复制代码
    // a.html
    <script>
      var worker = new SharedWorker("./worker.js")
    </script>
  • 一个 worker (任何类型) 和 她创建的 shared worker

    javascript 复制代码
    html
    复制代码
    // 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

    c 复制代码
    javascript
    复制代码
    // a.html
    <script>
      navigator.serviceWorker
        .register("service-worker.js", {
          scope: "./",
        })
    </script>
    
    
    // service-worker.js
    console.log("service-worker is working...");
  • 一个 Window 对象和一个 iframe 元素的 Window 对象(不同源)。

    less 复制代码
    javascript
    复制代码
    // 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 将变为空。

    css 复制代码
    html
    复制代码
    // 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 共享内存。

再推导:

简单案例

如代码,有这几个额,就叫做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>

引用

realm

agent

相关推荐
极客小俊27 分钟前
粘性定位Position:sticky属性是不是真的没用?
前端
云端看世界30 分钟前
ECMAScript 类型转换 下
前端·javascript
云端看世界32 分钟前
ECMAScript 运算符怪谈 下
前端·javascript
云端看世界33 分钟前
ECMAScript 函数对象实例化
前端·javascript
前端爆冲34 分钟前
基于vue和flex实现页面可配置组件顺序
前端·javascript·vue.js
云端看世界35 分钟前
ECMAScript 中的特异对象
前端·javascript
il37 分钟前
Deepdive into Tanstack Query - 2.1 QueryClient 基础
前端
_十六39 分钟前
看完就懂!用最简单的方式带你了解 TypeScript 编译器原理
前端·typescript
Komorebi_999941 分钟前
Axios 是一个基于 Promise 的 HTTP 客户端,可用于浏览器和 Node.js 环境。以下是它的一些主要作用
javascript·ajax
云端看世界41 分钟前
ECMAScript 运算符怪谈 上
前端·javascript·ecmascript 6