53

前言

JS 实在是太酷了(认真脸),那你有没有想过机器是怎么解析 JS 代码的?作为一个 JS 开发者,一般我们不需要直接跟编译器打交道,但是如果可以了解其中的基本原理,相信会对以后的工作和学习都有帮助的!

本篇介绍的知识主要基于 Node.js 和基于 Chromium 的浏览器所用的 V8 引擎

生成抽象语法树

HTML 解析器在遇到 script 标签时,便会加载其中的代码。代码可能是从 网络请求缓存 或者 Service Worker 中加载的。由于代码是以 字节流 的形式响应回来的,所以当代码下载完成后就会交给 字节流解码器

1-byte-stream.gif

词法分析

生成抽象语法树的 第一个阶段是分词(tokenize),又叫词法分析

字节流解码器会先从代码字节流中创建 令牌 (token)

注:令牌可以理解为语法上不可能再分的,最小的单个字符或字符串)。

如:0066 解码为 f0075 解码为 u0063 解码为 c0074 解码为 t0069 解码为 i006f 解码为 o006e 解码为 n 同时后面跟一个空格。然后你就得到了关键字 function

每当一个 令牌 创建后,就会被传递给 解析器(parser)。具体见下图:

2-to-parser.gif

语法分析

第二个阶段是解析(parse),也叫语法分析

引擎其实使用了两个解析器。一个是 预解析器,一个是 解析器

预解析器会先检查源码是否符合语法规则,如果不符合就直接抛出错误。这个提前检查机制可以提高解析器的效率。

如果没有错误,解析器便会根据传过来的令牌创建出 抽象语法树 (Abstract Syntax Tree) 并生成 执行上下文 (关于执行上下文的知识我们有机会再讲)

3-ast.gif

生成字节码

AST 被生成之后,接下来就要交给 解释器(interpreter) 了。解释器会遍历整个 AST,并生成 字节码。当字节码生成后,AST 便会被删除以节省内存空间。最终我们得到了更贴近 机器码字节码

这里的 字节码 是介于 AST机器码 之间的一种代码,它还是需要通过 解释器 将其转换为 机器码 后才能执行

4-byte-code.gif

执行代码

生成了字节码之后,就可以进入执行阶段了。执行阶段过程中引擎会做一些优化操作,一个是 即时编译,一个是 内联缓存

即时编译

尽管 字节码 很快,但是它还可以更快!解释器在逐条解释执行字节码时,会分析是否有某段代码被多次执行,这样的代码被称为 热点代码

热点代码 和生成的 类型反馈 (type feedback) 会被发送到一个称为 优化编译器 的东西中,然后由它转换为可以直接被电脑执行的 机器码,这样在下次执行这段代码的时候就不需要再编译了,从而大大提升了代码的执行效率。

这种技术也被称为 即时编译(JIT:Just In Time),而上面所说的 优化编译器 也叫 JIT 编译器

5-jit.gif

内联缓存

JavaScript 是一种动态类型的语言,这意味着数据类型可以不断变化。如果 JS 引擎每次都要检查数据的类型,那速度将会非常慢。

所以引擎就使用了一种叫做 内联缓存 (inline caching) 的技术。它将代码缓存在内存中,以便将来可以针对相同的行为直接返回缓存的值。比如你有一个函数调用了 100 次,每次都返回同一个值,那么引擎就会假定在 101 次时也返回该值。

假设我们有一个求和函数 sum,每次都接收两个数字:

6-sum.png

上面的函数返回值为 3!下次我们调用它时,引擎会假定我们还是传入两个数字类型的参数。

如果假设正确,就省去了动态查询阶段。引擎就可以直接使用存储在内存中的结果。否则,引擎会还原到原始字节码处解释执行,而不是使用优化过的机器码。

比如,下次我们要调用求和函数时,传入了一个字符串和一个数字,由于 JS 是动态类型的,所以不会报任何错误。

7-sum-2.png

这就意味着数字 2 会被转换成字符串,最终的结果将会变成 "12"。引擎会还原之前优化过的 只接收两个数字 的类型反馈,并重新返回到字节码处运行。


全文就到这里啦~本文是翻译的系列文章:

v8 部分的内容有参考极客时间的一个专栏 《浏览器工作原理与实践》:

专栏链接:浏览器工作原理与实践

如果你要买专栏的话,可以关注笔者的公众号,回复「极客时间」,我的返利全部返还哈~ 直接注册也能免费看五讲的~

参考链接


本文首发于公众号:码力全开(codingonfire)

本文随意转载哈,注明原文链接即可,公号文章转载联系我开白名单就好~

codingonfire.jpg


savokiss
6.2k 声望4.5k 粉丝

You know nothing, SpongeBob.