前言
最近看了下V8 binding相关的技术文档,主要是学习下chrome如何使用V8的isolate和context,所以就看了下英文文档,顺便翻译一下。英文文档链接https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md
这篇文章主要讲解V8 binding层的架构的几个关键概念,除了DOM wrappers的生命管理周期
目录
1. isolate (隔离)
2. Context (上下文)
3. Entered context and current context( 输入上下文和当前上下文)
4. World
5. A relationship between isolates, contexts, worlds and frames
6. DOM wrappers and worlds
7. DOM wrappers and contexts
isolate (隔离)
在V8中一个isolate是V8的一份实例。在blink中isolate和线程是1:1的关系。主线程与一个isolate相关联,一个工作线程与一个隔离相关联,但是也有个例外compositor worker是多个共享一个isolate
Context 上下文
Context是V8中全局变量范围的概念。简单的说,一个Window对象对应于一个Context。例如<iframe>和parent frame的有不同的Window对象,所以不同的frame具有不同的Context。由于每个Context创建了自己的全局变量和作用域,因此<iframe>的全局变量和原型链与parent frame的全局变量和原型链是隔离的
例子:
// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var foo = 1234;
String.prototype.substr =
function (position, length) { // Hijacks String.prototype.substr
console.log(length);
return "hijacked";
};
</script>
</body></html>
// iframe.html
<script>
console.log(foo); // undefined
var bar = "aaaa".substr(0, 2); // Nothing is logged.
console.log(bar); // "aa"
</script>
总之,每个frame都有一个Window对象。每个Window对象都有一个Context。每个Context都有自己的全局变量范围和原型链。
Entered context and current context
isolate和Context之间的关系是比较有趣的。一个isolate会在多个frame中执行JavaScripts,每个frame都有自己的context。这个意思就是一个isolate下的Context是会变化的。换句话说,isolate和Context之间的关系是1:N
这里我们有一个Entered context和current context的概念。要了解差异,您需要了解两种运行时堆栈:
第一个堆栈是JavaScript函数堆栈。该堆栈由V8管理。当一个函数调用另一个函数时,被调用函数入堆栈。当该函数返回时,该函数从堆栈出栈,然后返回到现在位于堆栈顶部的调用函数。每个函数都有一个相关的Context,我们将当前正在运行的函数的上下文(即,堆栈顶部的函数的上下文)称为current context
看以下例子:
// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe.onload = function () {
iframe.contentWindow.func();
}
</script>
</body></html>
// iframe.html
<script>
function func() {
...;
}
</script>
在上面的示例中,在运行func()时,current context指的是<iframe>的context。
第二个堆栈以更粗糙的粒度运行。该堆栈由V8绑定(而不是V8)管理。当V8绑定调用JavaScript时,V8绑定进入context并将context推送到堆栈。 JavaScript开始在context中运行。当JavaScript完成并且控件返回到V8绑定时,V8绑定会从堆栈中弹出上下文。鉴于V8绑定和V8之间的控制可以嵌套(即,V8绑定调用JavaScript,调用V8绑定,调用另一个JavaScript等),这些context形成堆栈。推送和弹出是由任何V8 API完成的,它采用上下文参数或显式调用v8 :: Context :: Enter()和v8 :: Context :: Exit()。我们将最近输入的context称为Entered context。
在上面的示例中,在运行func()时,Entered context是main frame的context(而不是<iframe>的context)。
Entered context是实现HTML规范的条目设置对象的概念。当前上下文是实现HTML规范的现任设置对象的概念。
总之,Entered context是从中开始当前JavaScript执行的context。current context是当前正在运行的JavaScript函数的context。
还有另一个称为调试器上下文的特殊上下文。如果调试器处于活动状态,则可以将调试器上下文插入到上下文堆栈中
World
World是在Chrome扩展的内容脚本中沙盒DOM wrappers的概念. 这里三种World类型:
1. main world
2. isolated world
3.worker world
main world用于执行网页的JavaScript脚本
isolated world用于执行Chrome扩展程序的内容脚本
主线程的isolate有1个main world和N个isolated worlds
work thread只有1 worker world ,但是没有isolated world
下图有助于理解他们之间的关系
一个isolate中的所有Worlds共享底层C ++ DOM对象,但每个World都有自己的DOM wrappers 。这样一个隔离区中的世界可以在相同的C ++ DOM对象上运行,而无需在World中共享任何DOM wrapper
每个World都有自己的上下文。这意味着每个World都有自己的全局变量范围和原型链
作为沙盒的结果,一个isolate中的World不能共享任何DOM wrappers 或上下文,但可以共享底层C ++ DOM对象。没有共享DOM wrappers 或上下文这一事实意味着World之间不会共享任何JavaScript对象。这样我们就可以保证Chrome扩展在共享底层C ++ DOM对象时不共享任何JavaScript对象的安全模型。此沙箱允许Chrome扩展在共享DOM结构上运行不受信任的JavaScripts。
(注意: isolated world是V8绑定的概念,而isolate和上下文是V8的概念.V8不知道isolated worlds 是什么。)
总之,主线程的isolate由1个main world和N 个isolated worlds组成。worker thread的isolate 由1个 worker world 和 0 isolated world组成。一个隔离中的所有World共享底层的C ++ DOM对象,但每个World都有自己的DOM wrappers。每个World都有自己的上下文,因此有自己的全局变量范围和原型链。
isolates, contexts, worlds and frames 之间的关系
总结一下isolates,contexts,worlds 和frames之间的关系:
DOM端的要求,一个HTML页面具有N个frame。每个frame都有自己的context
JavaScript端的要求,一个isolate具有M个world。每个world都有context
结果,当我们执行涉及N Frame 和M个world的主线程时,存在N * M个上下文。换句话说,为每对(frame,world)创建一个上下文。下图有助于理解这种关系:
主线程一次只能有一个current context,但主线程在其生命周期内可以有N * M个上下文。例如,当主线程使用World Y中的JavaScript在frame X上操作时,当前上下文被设置为(X,Y)对的上下文。主线程的当前上下文在其生命周期中发生变化。
另一方面,work thread 有0 frame 和1 World。因此,工作线程只有1个context。工作线程的current context永远不会改变。
DOM wrappers and contexts
出于兼容性原因,只要底层C ++ DOM对象处于活动状态,我们就需要确保将相同的DOM wrapper 返回给JavaScript。我们不应该为同一个C ++ DOM对象返回不同的DOM包装器。
以下是代码例子
var div = document.createElement("div");
div.foo = 1234; // expando
var p = document.createElement("p");
p.appendChild(div);
div = null;
gc();
console.log(p.firstChild.foo); // This should be 1234, not undefined
要实现只要底层C ++ DOM对象处于活动状态,同一DOM wrapper 返回到JavaScript的语义,我们需要从C ++ DOM对象到DOM包装器的映射。另外,我们需要在每个world中沙箱DOM wrapper 。为了满足这些要求,我们让每个world都拥有一个DOM wrapper 存储,它存储从C ++ DOM对象到该世界中DOM wrapper 的映射。
因此,我们在一个隔离中有多个DOM wrapper 存储。main World 的映射用ScriptWrappable编写。如果ScriptWrappable :: main_world_wrapper_具有非空值,则它是main World 的C ++ DOM对象的DOM wrapper 。其他world的映射是在DOMWrapperMap中编写的。
DOM wrappers and contexts
创建新的DOM wrapper 时,需要选择创建DOM wrapper 的正确上下文。如果在错误的上下文中创建新的DOM包装器,最终会将JavaScript对象泄漏到其他上下文,这很可能会导致安全问题。
// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe; // The wrapper of the iframe should be created in the context of the main frame.
iframe.contentDocument; // The wrapper of the document should be created in the context of the iframe.
iframe.contentDocument.addEventListener("click",
function (event) { // The wrapper of the event should be created in the context of the iframe.
event.target;
});
</script>
</body></html>
// iframe.html
<script>
</script>
要确保在正确的上下文中创建DOM wrapper ,您需要确保在调用ToV8()时必须将当前上下文设置为正确的上下文。
转载注明出处:
麦子:V8 bindings 设计isolate,context,world,frame之间的关系(翻译)zhuanlan.zhihu.com