Node.js 底层#4 - 谈谈 V8
在我们之前的文章中,我们讨论了 JavaScript 和 JavaScript 引擎的最后一点。
现在我们已经深入 Node.js 的底层,事情开始变得混乱和复杂。我们开始讨论 JavaScript,这是我们所掌握的更高层次的概念,然后我们又讲了一些概念,例如:调用堆栈、事件循环、堆、队列等等……
问题是:这些东西实际上都不是 JS 实现的,它们都是引擎的一部分。所以 JavaScript 本质上是一种动态类型的解释型语言,我们在 JavaScript 中运行的所有内容都会传递给引擎,引擎会与其环境交互并生成机器运行程序所需的字节码。
而这款发动机就叫做V8。
什么是 V8
V8 是 Google 的开源高性能 JavaScript 和 WebAssembly 引擎。它用 C++ 编写,可在 Chrome 或类似 Chrome 的环境以及 Node.js 中使用。V8 拥有 ECMAScript 和 WebAssembly 的完整实现。但它不依赖于浏览器,实际上,V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中。
概述
V8 最初的设计初衷是为了提升 Web 浏览器中 JavaScript 的执行性能——这也是 Chrome 浏览器在当时与其他浏览器相比速度差距巨大的原因。为了实现这一性能提升,V8 所做的不仅仅是解释 JavaScript 代码,它还会将这些代码转换为更高效的机器码。它通过实现所谓的JIT(Just In Time,即时编译器)编译器,在运行时将 JS 编译成机器码。
到目前为止,大多数引擎的工作方式实际上都相同,V8 与其他引擎最大的区别在于它根本不生成任何中间代码。它首次运行代码时,会使用名为 Ignition 的未优化编译器,直接将代码编译成应有的读取方式。之后,经过几次运行后,另一个编译器(JIT 编译器)会获取大量关于代码在大多数情况下实际行为的信息,并重新编译代码,使其根据当时的运行方式进行优化。这基本上就是对某些代码进行“JIT 编译”的含义。与 C++ 等使用 AoT (提前)compile
编译的其他语言不同,这意味着我们先编译,生成可执行文件,然后运行它。Node中没有任务。
V8 还使用了许多不同的线程来提高速度:
- 主线程负责获取、编译和执行 JS 代码
- 另一个线程用于优化编译,因此主线程在前一个线程优化正在运行的代码时继续执行
- 第三个线程仅用于分析,它告诉运行时哪些方法需要优化
- 其他一些线程用于处理垃圾收集
抽象语法树
几乎所有语言的编译流程的第一步都是生成所谓的AST(抽象语法树)。抽象语法树是以抽象形式呈现给定源代码语法结构的树形表示,这意味着理论上它可以被翻译成任何其他语言。树中的每个节点都表示源代码中出现的一种语言结构。
让我们回顾一下我们的代码:
const fs = require('fs')
const path = require('path')
const filePath = path.resolve(`../myDir/myFile.md`)
// Parses the buffer into a string
function callback (data) {
return data.toString()
}
// Transforms the function into a promise
const readFileAsync = (filePath) => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, (err, data) => {
if (err) return reject(err)
return resolve(callback(data))
})
})
}
(function start () {
readFileAsync(filePath)
.then()
.catch(console.error)
})()
这是我们代码中的一个 JSON 格式的示例 AST(其中的一部分),由名为esprimareadFile
的工具生成:
{
"type": "Program", // The type of our AST
"body": [ // The body of our program, an index per line
{
"type": "VariableDeclaration", // We start with a variable declaration
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier", // This variable is an identifier
"name": "fs" // called 'fs'
},
"init": { // We equal this variable to something
"type": "CallExpression", // This something is a call expression to a function
"callee": {
"type": "Identifier", // Which is an identifier
"name": "require" // called 'require'
},
"arguments": [ // And we pass some arguments to this function
{
"type": "Literal", // The first one of them is a literal type (a string, number or so...)
"value": "fs", // with the value: 'fs'
"raw": "'fs'"
}
]
}
}
],
"kind": "const" // Lastly, we declare that our VariableDeclaration is of type const
}
]
}
因此,正如我们在 JSON 中看到的,我们有一个名为的打开键type
,它表示我们的代码是Program
,并且我们有它的body
。body
键是一个对象数组,其中每个索引代表一行代码。我们的第一行代码是,const fs = require('fs')
所以它是数组的第一个索引。在第一个对象中,我们有一个type
键,表示我们正在做的是变量声明,以及这个特定变量的声明(因为我们可以做const a,b = 2
,所以declarations
键是一个数组,每个变量一个)fs
。我们有一个type
名为VariableDeclarator
,它标识我们正在声明一个名为的新标识符fs
。
之后,我们初始化变量,也就是init
key,它表示从=
符号开始的所有内容。keyinit
是另一个对象,它定义了我们正在调用一个名为的函数require
,并传递一个 value 的字面参数fs
。所以基本上,这整个 JSON 定义了我们一行代码。
AST 是所有编译器的基础,因为它允许编译器将高层表示(代码)转换为低层表示(树),从而删除我们放入代码中的所有无用信息,例如注释。此外,AST 还允许我们(普通程序员)随意修改代码,这基本上就是智能感知或其他代码助手的功能:它会分析 AST,并根据您目前编写的内容,建议后续可以添加的代码。AST 还可以用来动态替换或修改代码,例如,我们只需查看中的键值,就可以将所有 替换let
为。const
kind
VariableDeclaration
如果 AST 使我们能够识别性能问题并分析代码,那么它对编译器也同样如此。这就是编译器的本质:分析、优化并生成可由机器运行的代码。
结论
这是我们关于 V8 及其工作原理的讨论的开始!我们将讨论字节码和其他许多有趣的内容!敬请期待下一章 :D
文章来源:https://dev.to/_staticvoid/node-js-under-the-hood-4-let-s-talk-about-v8-1eol