- 汇编器 编译器 解释器
- 解释执行和解释执行
- 什么是V8?
- V8执行Js代码的过程
汇编器 编译器 解释器
众所周知,计算机只能理解机器语言,而我们平时编程用的通常是高级语言,所以源代码通常都要经过层层转换最终变成机器语言运行。
早期只有汇编语言没有高级语言,不同的设备有一套自己的对应不同机器语言指令集的汇编语言,也就是说,汇编语言不能在不同CPU架构(intel ARM MIPS)之间移植。同一个软件为了让不同类型的处理器架构都能兼容要写好几套代码,实在太不方便了,我们需要一种屏蔽了计算机架构细节的语言,能适应多种不同CPU架构的语言,专心处理业务逻辑,所以后来发展出了跨平台的高级语言。诸如 C C++ Java JavaScript。
随着计算机发展,编译器也越来越复杂,发展了很多分支,像是本地编译器、交叉编译器等,这里就不多说了。
那么源码一定要经过编译才能运行吗?
解释器的出现给出了一种不用编译就能运行的能力
先静态编译到可执行的文件,然后运行可执行的文件来执行程序,而解释器提供了一种边编译边运行的动态运行方法,而也正因为通过解释器运行的代码是边编译边运行的,所以运行的速度比静态编译的那种慢很多。
所以程序运行的方式分为编译执行和解释执行。
解释执行和解释执行
解释执行:需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果。
第二种是编译执行。采用这种方式时,也需要先将源代码转换为中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。
什么是V8
JavaScript引擎是执行JavaScript代码的程序或解释器。javaScript引擎可以实现为标准解释器或即时编译器,它以某种形式将JavaScript编译为字节码。
那么除了v8引擎,你还知道那些js引擎
V8 - 开源,由Google开发,用C ++编写
Rhin- 由Mozilla基金会开源,完全用Java开发
SpiderMonkey 第一个JavaScript引擎,Netscape Navigator,Firefox
JavaScriptCore 苹果公司为Safari开发
KJS 最初由Harri Porten为KDE项目的Konqueror网络浏览器开发
Chakra** (JScript9) Microsoft Edge
Chakra** (JavaScript) Microsoft IE9-IE11
Nashorn 作为OpenJDK的一部分,由Oracle Java语言和工具组编写
JerryScript 一个物联网的轻量级引擎
V8 是用 C++ 写的,使用了即时编译技术,工作模式如下图
在这个过程中,V8同时使用了Parser(解析器)、Ignition(解释器) 和TurboFan(编译器) 来执行Js代码。V8是被设计用来提高网页浏览器内部JavaScript执行的性能,那么如何提高性能呢?
为了提高性能,v8会把js代码转换为高效的机器码,而不在是依赖于解释器去执行。v8引入了 JIT在运行时把js代码进行转换为机器码。这里的主要区别在于V8不生成字节码或任何中间代码。
v8曾经有两个编译器(v5.9之前)
full-codegen — 一个简单且速度非常快的编译器,可以生成简单且相对较慢的机器码 Crankshaft — 一个更复杂的(Just-In-Time)优化编译器,生成高度优化的代码
v8充分多进程,主进程负责获取代码,编译生成机器码,有专门负责优化的进程,,还有一个监控进程负责分析那些代码执行比较慢,以遍Crankshaft 做优化,最后还有一个就是GC进程,负责内存垃圾回收。
V8执行Js代码的过程
1 Parser生成抽象语法树
在Chrome中开始下载Javascript文件后,Parser就会开始并行在单独的线程上解析代码。这意味着解析可以在下载完成后仅几毫秒内完成,并生成AST。
AST还广泛应用于各类项目中,比如Babel、ESLint,以及我们面试常说手写loader,那么AST的生成过程是怎么样的呢?
我们首先看一下Babel的实现:
// 解析
let esprima = require('esprima')
// 转换
let estraverse = require('estraverse')
// 生成
let escodegen = require('escodegen')
let code = `function code () {}`
// 解析
let ast = esprima.parseScript(code)
// 转换
estraverse.traverse(ast, {
enter (node) {
console.log('enter' + node.type)
if (node.type === 'enterIdentifier') {
node.name = 'hello'
}
},
leave (node) {
console.log('enter' + node.type)
}
})
// 生成
let newCode = escodegen.generate(ast)
我们常说的生成AST不管是Babel 还是vue中 都是 解析这一步:
解析步骤接收代码并输出 AST。这个步骤分为两个阶段:词法分析(Lexical Analysis)和 语法分析(Syntactic Analysis)。
词法分析:将代码(字符串)分割为token流,即语法单元成的数组 语法分析:分析token流(上面生成的数组)并生成 AST 转换
var name = "react"
//转成token后为
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "name"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "String",
"value": "\"react\""
},
{
"type": "Punctuator",
"value": ";"
}
]
从上面可以看出,var name = "ivweb"; 这样一段代码,会有关键字"var"、标识符"name"、赋值运算符"="、字符串"ivweb"、分隔符";",共5个token。
2.Ignition生成字节码
解释器 Ignition 根据语法树生成字节码。TurboFan 是 V8 的优化编译器,TurboFan 将字节码生成优化的机器代码。
如果想知道为什么会有两种执行模式?点击查看视频 [](Franziska Hinkelmann: JavaScript engines - how do they even? | JSConf EU 2017)
字节码是机器代码的抽象。如果字节码采用和物理 CPU 相同的计算模型进行设计,则将字节码编译为机器代码更容易。这就是为什么解释器(interpreter)常常是寄存器或堆栈。Ignition 是具有累加器的寄存器。
您可以将 V8 的字节码看作是小型的构建块(bytecodes as small building blocks),这些构建块组合在一起构成任何 JavaScript 功能。V8 有数以百计的字节码。比如 Add 或 TypeOf 这样的操作符,或者像 LdaNamedProperty 这样的属性加载符,还有很多类似的字节码。V8还有一些非常特殊的字节码,如 CreateObjectLiteral 或 SuspendGenerator。头文件 bytecodes.h 定义了 V8 字节码的完整列表。
每个字节码指定其输入和输出作为寄存器操作数。Ignition 使用寄存器 r0,r1,r2,... 和累加器寄存器(accumulator register)。几乎所有的字节码都使用累加器寄存器。它像一个常规寄存器,除了字节码没有指定。例如,Add r1 将寄存器 r1 中的值和累加器中的值进行加法运算。这使得字节码更短,节省内存。
许多字节码以 Lda 或 Sta 开头。Lda 和 Stastands 中的 a 为累加器(accumulator)。例如,LdaSmi [42] 将小整数(Smi)42 加载到累加器寄存器中。Star r0 将当前在累加器中的值存储在寄存器 r0 中。
以现在掌握的基础知识,花点时间来看一个具有实际功能的字节码。
function incrementX(obj) {
return 1 + obj.x;
}
incrementX({x: 42});
// V8 的编译器是惰性的,
// 如果一个函数没有运行,V8 将不会解释它
ps 如果要查看 V8 的 JavaScript 字节码,可以使用在命令行参数中添加 --print-bytecode 运行 D8 或Node.js(8.3 或更高版本)来打印。对于 Chrome,请从命令行启动 Chrome,使用 --js-flags="--print-bytecode",请参考 Run Chromium with flags。
$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
12 E> 0x2ddf8802cf6e @ StackCheck
19 S> 0x2ddf8802cf6f @ LdaSmi [1]
0x2ddf8802cf71 @ Star r0
34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4]
28 E> 0x2ddf8802cf77 @ Add r0, [6]
36 S> 0x2ddf8802cf7a @ Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
- map = 0x2ddfb2d02309
- length: 1
0: 0x2ddf8db91611
Handler Table (size = 16)
3.执行代码及优化
Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan,然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition。
其实字节码配合解释器和编译器是很火的一种技术,例如 Python 和 Java 的虚拟机也是基于这种技术,我们把这种技术称为即时编译(JIT)。
这么多语言和工作引擎都使用了“字节码 + JIT”技术,其具体工作流程如下:
推荐阅读
(点击标题可跳转阅读)
RxJS入门
一文掌握Webpack编译流程
一文深度剖析Axios源码
Javascript条件逻辑设计重构Promise知识点自测你不知道的React Diff你不知道的GIT神操作程序中代码坏味道(上)
程序中代码坏味道(下)
学习Less,看这篇就够了
一文掌握GO语言实战技能(一)
一文掌握GO语言实战技能(二)
一文掌握Linux实战技能-系统管理篇
一文掌握Linux实战技能-系统操作篇
一文达到Mysql实战水平
一文达到Mysql实战水平-习题答案
从表单抽象到表单中台
vue源码分析(1)- new Vue
实战LeetCode 系列(一) (题目+解析)
一文掌握Javascript函数式编程重点
实战LeetCode - 前端面试必备二叉树算法
一文读懂 React16.0-16.6 新特性(实践 思考)
阿里、网易、滴滴、今日头条、有赞.....等20家面试真题
30分钟学会 snabbdom 源码,实现精简的 Virtual DOM 库
觉得本文对你有帮助?请分享给更多人
关注「React中文社区」加星标,每天进步