在本文中,我们将探讨JavaScript运行时的所有重要方面。我们的目的是了解JavaScript运行时的一般内部工作,涵盖了所有重要细节。让我们深入研究!
体系结构概述
JavaScript是一种动态的,单线的,JIT编译的,面向堆栈的编程语言。我在其他文章中介绍了该语言的技术细节:Technical introduction to JavaScript。重要的是要事先理解这些方面以充分利用本文。
让我们从浏览器运行时的一般体系结构图开始:
在上图中,彩色部分是核心JavaScript。其他一切都是负责成功运行JavaScript代码的平台的环境。
每当我们运行JavaScript代码时,我们都会在某种环境中运行它,这不是语言规范的一部分。 JavaScript环境是一个系统,可以使用 Engine 来促进JavaScript代码执行,并通过提供不同的功能和API来扩展JavaScript的功能。例如:
-
nodejs 是一个JavaScript环境以及更多。
-
Chromium 基于浏览器(例如 Google Chrome )是JavaScript环境,使用V8引擎,与 nodejs 相同,并提供Web API(
document
,window
等)执行用于创建强大动态网站的前端特定任务。
其他著名的浏览器具有自己的Web API和引擎实现。例如。 Microsoft的Edge 使用脉轮
JavaScript是一种单线程语言,这意味着它一次只能执行一个任务。执行程序后,执行的函数被一个接一个地称为执行。为了跟踪执行状态和函数调用,JavaScript使用称为call-stack的堆栈数据结构。为了更好地理解它,让我们考虑一个例子: 运行上述代码时,每个函数调用将函数推入呼叫堆栈。函数完成执行后,它们将从呼叫堆栈中弹出。 上面的代码是同步执行的,这意味着所有执行都立即在给定时间点,而没有控制转移到其他某些实体。相反,让我们修改我们的示例以了解异步代码行为。 我们可以使用JavaScript开发异步代码,因为 事件循环 和 call-back queue
JavaScript代码执行
function multiply(x,y){
return x * y;
}
function square(n){
return multiply(n, n)
}
let a = 10;
console.log(square(a))
通过运行时环境(请参见上面的体系结构图)。 JavaScript Runtimes提供的这些功能启用了编写异步代码的用途。 JavaScript本身就是一种阻止同步语言。
在下面的代码中,我们使用名为setTimeout
的Web API的函数来创建异步行为。它使我们能够在提到的毫秒(第二个参数)中提到的时间后调用函数(以第一个参数为setTimeout
),因此使代码异步。
function multiply(x,y){
return x * y;
}
function square(n){
return multiply(n, n);
}
let a = 10;
setTimeout(function later(){console.log(square(a))}, 1000);
console.log('Hi')
执行后,函数later
被发送到呼叫队列, 在1000
毫秒后或一秒钟后将其回电 。这就是为什么当我们运行代码时,我们首先看到Hi
,然后至少在一秒钟后,输出中的100
。解释代码时,异步功能将发送到回调队列。一旦呼叫堆为空,将这些推动将其堆叠起来,这是由 事件循环 完成的。
到目前为止,我们对JavaScript运行时如何在同步和异步操作中起作用。如果您对进一步的探索感兴趣,JS 900是探索同步和异步语言行为的绝佳工具。
p>
JavaScript代码生命周期
让我们更深入地了解一个级别,以了解如何端到端进行执行的代码,即从源文件一直到生成机器代码并执行它。
当我们在上面讨论的任何环境中运行JS程序时,它最终在执行之前就进入了不同的阶段。这些是 tokenisation ,解析,代码生成,最后 code-execution 。
新手开发人员经常声称JavaScript是一种解释的语言,但是正如我们将在下面看到的那样,这只是一半。
任何JavaScript程序始终被象征化并解析以生成一个中间形式的代码,称为 byte-code ,然后由 internemer 解释以生成可执行的机器代码。这与将源代码直接转换为机器代码进行执行的纯粹解释的语言不同。 (Difference between Byte Code and Machine Code)
在解释和执行之前,将JavaScript程序动态编译为字节代码。这就是为什么JavaScript是一种“及时”(JIT)编译的语言。请注意,我们使用了术语编译的术语。这是为了反思以下事实:JavaScript引擎没有预编译JavaScript并以后运行的奢侈品(也称为 static 编译或 (AOT)编译)与其他编译的语言一样。让我们现在详细了解整个行话。要设置前提,正在考虑的JavaScript运行时是V8,由基于铬的浏览器和Nodejs使用。
我们从示例JavaScript源代码开始。
var sayHello = 'Hello World!';
function printMessage(message){
console.log(message)
}
printMessage(sayHello)
引擎开始定位源代码。通常,此代码要么在文件,Web服务器中或以HTML <script>
标签中嵌入的脚本中存在。
象征化
识别后,源代码被标记了。该过程称为 tokenisation 。在此过程中,JavaScript引擎将句法源代码转换为令牌。这是突出显示语法中错误的过程。简而言之,源代码被分解为较小的有效语言构造。例如。 var sayHello = 'Hello World!';
被分解为var
一个要声明功能示波器变量的关键字,sayHello
标识符,=
操作员和'Hello World'
字符串文字,最后是;
。
在某些编译的语言中,此过程也称为 lexing 。
解析
产生令牌后,它们将传递给A Parser 作为输入,该输入执行解析。解析是生成抽象语法树(AST)和执行范围的过程。生成AST允许我们表示程序的层次结构。解释器在生成字节代码的后续步骤中使用了此层次结构。
请注意,AST不直接包含文字或变量的值。我们将稍后再介绍。
对于V8引擎,分析有两种类型:急切的解析和懒惰解析。急切的解析同时生成AST和执行范围,而懒惰解析仅生成AST。
执行示波器,简单的术语是将标识符映射到其分配值的地方。对于我们的示例,
a
映射到10
数字文字。此信息存储在执行范围中,而不是直接将它们放入AST中,以使AST Generation保持快速的过程。
急切的解析用于顶级代码,该代码将立即由解释器立即执行,而懒惰解析是针对嵌套的代码和未调用的声明,因此尚不需要执行程序。
var sayHello = 'Hello World!'; // eagerly parsed
// eagerly parsed, but everything inside is lazy parsed for now
function printMessage(message){
console.log(message)
}
// oh shit, need to parse everything eagerly inside, as the function is called.
printMessage(sayHello)
在急切的解析中,生成的执行示波器用于将标识符值分配给 actible proxies inst Inside (如上图所述),该值在AST生成时没有分配给它们的任何值。懒惰解析的AST留在没有分配标识符值的情况下,以使其更快地创建,从而加快解析过程。将标识符值从执行范围分配到AST中变量代理的过程称为范围分析。
根据V8术语,懒惰解析是由A Pre-Pre-Parser 进行的。预先分类者的速度是解析器的两倍,因为它们跳过执行范围生成和价值分配。
代码生成和执行
一旦生成AST并进行水合(带有值分配的变量代理),它将传递给A 字节代码生成器。它负责一次产生一个字节代码流,一次是一个字节代码。一旦生成字节代码,解释器最终将其解释以产生机器代码(0s和1s)。然后执行机器代码以产生程序输出。
p>
关于JavaScript优化的单词
作为附带说明,讨论JavaScript引擎采用的某些技术以加快代码编译和执行过程,这是一个很好的选择。 Javascrip引擎能够在多次运行代码时优化代码运行。
正如我们已经讨论的那样,一旦水合AST被馈送到解释器,它就会开始生成字节代码流。在这样做的过程中,输出流将传递给'优化编译器',该负责生成优化的机器代码版本,具体取决于其从先前的执行运行中获得的信息。以下是该过程的非常通用的图表。整个过程称为执行和优化管道。
解释器正在生成字节代码时,它还存储某种 proporging数据,可在后续运行中使用。当函数变为'HOT'(即多次运行时)时,该函数生成的字节代码以及分析数据将传递给优化编译器以创建该功能的“直列缓存” 将在未来运行中直接换于字节代码流中,而不是再生字节代码。
某些引擎具有多个优化编译器,而不仅仅是一个。从本质上讲,所有优化和缓存技术都在JavaScript引擎能够'profile' 对象形状(JavaScript中的所有事物都是对象)的事实。文章:JavaScript Shapes and Inline Caches详细说明了整个优化过程。
p>
结论
我们进入JavaScript运行时的旅程已经揭开了其核心机制,从体系结构到代码执行和生命周期。我们已经掌握了它的动态,单线程的性质,环境的作用和执行环境。了解同步和异步代码执行,事件循环的角色以及从源代码到机器代码中的生命周期,我们将JavaScript的本质视为一种“正常的”编译语言。此外,已经探索了诸如内联缓存之类的优化技术,展示了提高性能的复杂策略。
直到下一次:)