前言:
现时咱们对“js运行程序”都比较关切,各位老铁们都想要剖析一些“js运行程序”的相关知识。那么小编也在网上汇集了一些有关“js运行程序””的相关知识,希望你们能喜欢,同学们快快来了解一下吧!今天我们将深入了解JavaScript的V8引擎,并弄清楚JavaScript是如何执行的。
在中,我们了解了浏览器的结构,并对。让我们来回顾一下,这样有利于我们进行更深入的研究。
背景
Web标准是一系列浏览器实现的规则。它们定义和描述了万维网的各个方面。
W3C是一个为Web领域开发开放标准的国际组织。他们确保每个开发者都遵循相同的准则,而无须支持许多完全不同的环境。
现代浏览器是一个相当复杂的软件,它的代码库有数千万行代码。所以它被分成了很多负责不同逻辑的模块。
浏览器最重要的两个部分是JavaScript引擎和渲染引擎。
Blink是一个渲染引擎,负责整个渲染管线(包括DOM树、样式、事件和V8集成),并解析DOM树,解析样式,并确定所有元素的视觉几何形状。
在通过动画帧持续监控动态变化的同时,Blink会将内容绘制在屏幕上。JS引擎是浏览器的一个重要组成部分——但我们还没有讨论到这些细节。
JavaScript引擎101
JavaScript引擎执行JavaScript并将其编译成原生机器代码。每个主流浏览器都开发了自己的JS引擎:谷歌的Chrome使用V8,Safari使用JavaScriptCore,Firefox使用SpiderMonkey。
本文使用的是V8,因为它在Node.js和Electron中可以使用,但其他引擎的构建也是类似的。
每个步骤都有一个指向负责该步骤的代码链接,这样您就可以熟悉代码库,并且可以继续本文之外的研究。
我们使用,因为它提供了一个方便和知名的UI来浏览代码库。
准备源代码
V8需要做的第一件事是下载源代码。可以通过网络、缓存或service worker来完成。
一旦获取到代码,我们需要以编译器能够理解的方式对其进行更改。这个过程称为解析,由两部分组成:扫描器和解析器本身。
扫描器获取JS文件并将其转换为内置的令牌列表。在keywords.txt文件中有一个所有JS令牌的列表。
解析器拿到令牌列表,然后创建抽象语法树(AST):以树形来表示源代码。树的每个节点表示代码中出现的一个结构。
让我们看一个简单的例子:
function foo() {
let bar = 1;
return bar;
}
这段代码将生成以下树结构:
可以通过执行前序遍历(根,左,右)来执行这段代码:
1. 定义foo函数
2. 声明bar变量
3. 把1赋值给bar
4. 从函数中返回bar。
您还将看到VariableProxy—一个将抽象变量连接到内存中某个位置的元素。解析VariableProxy的过程称为作用域分析。
在我们的示例中,该过程的结果将是所有VariableProxys都指向相同bar变量。
即时编译机制
通常,要运行代码,就需要将编程语言转换为机器代码。对于如何以及何时发生这种转变,有几种方法。
转换代码最常见的方法是执行预编译。它的工作正如它的字面意思:在编译阶段执行程序之前,代码被转换为机器代码。
这种方法被许多编程语言使用,比如C++、Java和还有一些其他语言。
此外需要说明一下:代码的每一行都将在运行时执行。动态类型语言(如JavaScript和Python)通常采用这种方法,因为在执行之前不可能知道确切的类型。
因为预编译可以一起评估所有代码,所以它可以提供更好的优化,并最终生成性能更好的代码。另一方面,解释实现起来更简单,但它通常比编译好的代码更慢。
为了更快更有效地转换动态语言的代码,创建了一种称为即时(JIT)编译的新方法。它充分结合了解释和编译。
在使用解释作为基本方法时,V8可以检测到使用频率较高的函数,并使用以前执行的类型信息编译它们。
然而,类型可能会发生变化。我们需要对编译后的代码去优化,转而回退到解释(之后,我们可以在获得新的类型反馈后重新编译函数)。
让我们来更详细地探讨JIT编译的每个部分。
解释器
V8使用一个名为Ignition的解释器。最初,它采用抽象语法树并生成字节码。
字节码指令也有元数据,例如用于将来调试的源行位置。通常,字节码指令与JS抽象相匹配。
现在让我们为上面的例子手动生成字节码:
LdaSmi #1 // write 1 to accumulator
Star r0 // read to r0 (bar) from accumulator
Ldar r0 // write from r0 (bar) to accumulator
Return // returns accumulator
Ignition有一个叫做累加器的东西——一个你可以存/取值的地方。
这个累加器避免了入栈和出栈的需要。它也是许多字节码的隐式参数,通常保存操作的结果。Return隐式返回累加器。
您可以在相应的源代码中检出所有相关字节码。如果你对其他JS概念(如循环和async/await)如何在字节码中呈现感兴趣,我发现阅读这些例子很有用。
执行
在生成后,Ignition使用一个由字节码键控的处理程序表来解释指令。对于每个字节码,Ignition可以查找相应的处理程序函数并传入提供的参数,然后执行。
正如前面提到的,执行阶段还提供了代码的类型反馈。让我们来搞明白它是如何收集和管理的。
首先,我们应该讨论JavaScript对象是如何在内存中表示的。一个简单的方法是,可以为每个对象创建一个字典并将其链接到内存。
然而,我们通常有很多具有相同结构的对象,因此存储大量重复的字典效率不高。
为了解决这个问题,V8使用Object Shapes (或内部的映射)和内存中的值向量将对象的结构与值本身分离。
例如,我们创建一个对象字面值:
let c = { x: 3 }
let d = { x: 5 }
c.y = 4
第一行中,生成一个结构Map[c],其属性为x,偏移量为0。
第二行中,V8将为一个新变量重用相同的结构。
第三行中,为属性y创建一个偏移量为1的新结构Map[c1],并创建一个到前一个结构Map[c]的链接。
在上面的例子中,你可以看到每个对象都有一个指向对象形状的链接,对于每个属性名,V8可以在内存中找到值的偏移量。
对象结构本质上是链表。如果你写c.x, V8会去到列表的头,在那里找到y,移动到连接的结构,最后获取x并从中读取偏移量。然后它会去内存向量并返回它的第一个元素。
可想而知,在大型web应用中,你会看到大量相互连接的形状。同时,在链表中搜索需要线性时间,这使得属性查找成为非常昂贵的操作。
为了在V8中解决这个问题,你可以使用内联缓存(Inline Cache, IC)。它会记住在哪里查找对象属性的信息,以减少查找的次数。
您可以将其视为代码中的监听站点:它跟踪函数中的所有CALL、STORE和LOAD事件,并记录所有经过的形状。
保存IC的数据结构称为反馈向量。它只是一个数组,用来保存函数的所有IC。
function load(a) {
return a.key;
}
对于上面的函数,反馈向量看起来像这样:
[{ slot: 0, icType: LOAD, value: UNINIT }]
这是一个简单的函数,只有一个IC,其类型为LOAD,值为UNINIT。这意味着它是未初始化的,我们不知道接下来会发生什么。
用不同的参数调用这个函数,看看内联缓存将如何改变。
let first = { key: 'first' } // shape A
let fast = { key: 'fast' } // the same shape A
let slow = { foo: 'slow' } // new shape B
load(first)
load(fast)
load(slow)
在第一次调用load函数之后,我们的内联缓存将得到一个更新的值:
[{ slot: 0, icType: LOAD, value: MONO(A) }]
这个值现在变成了单态的,这意味着这个缓存只能解析成结构A。
在第二次调用之后,V8将检查IC的值,它将看到它是单态的,并且具有与快速变量相同的形状。它会很快返回offset并解析它。
第三次,结构与存储的不同。因此V8将手动对其进行解析,并将值更新为具有两种可能结构的数组的多态状态。
[{ slot: 0, icType: LOAD, value: POLY[A,B] }]
现在,每当我们调用这个函数时,V8不仅需要检查一个结构,还需要遍历几种可能性。
为了代码更快,可以初始化具有相同类型的对象,而不需要过多地更改它们的结构。
注意:您可以记住这一点,但如果它会导致代码重复或表达性较差的代码,就不要这样做。
内联缓存还会跟踪调用它们的频率,以决定它是否是优化编译器的良好候选者——Turbofan。
编译器
Ignition就到此为止。如果一个函数会被调用多次,这个函数会在编译器Turbofan中进行优化,使其变得更快。
Turbofan从Ignition获取字节码,并将类型反馈(反馈向量)用于函数,在此基础上应用一系列缩减,并生成机器代码。
正如我们前面看到的,类型反馈并不能保证它在将来不会改变。
例如,Turbofan优化的代码基于一个假设,即某些加法总是加整数。
但是如果它接收到一个字符串会发生什么呢?这个过程被称为去优化。丢弃优化的代码,回到解释代码,继续执行,并更新类型反馈。
总结
在本文中,我们讨论了JS引擎的实现以及如何执行JavaScript的确切步骤。
总之,让我们从头再来看一看编译管线。
V8概览
我们来逐步地回顾:
1. 一切都从从网络获取JavaScript代码开始。
2. V8解析源代码并将其转换为抽象语法树(AST)。
3. 基于这个AST, Ignition解释器可以开始做它的事情并产生字节码。
4. 此时,引擎开始运行代码并收集类型反馈。
5. 为了使它运行得更快,可以将字节代码与反馈数据一起发送到优化编译器。优化编译器在此基础上进行某些假设,然后生成高度优化的机器代码。
6. 如果在某个时刻,其中一个假设被证明是不正确的,优化编译器就会去优化并返回到解释器过程。
完结撒花!如果您对某个特定阶段有任何疑问或想了解更多细节,您可以深入源代码或在Twitter上与我联系。
标签: #js运行程序