动图学 JavaScript 之: JS 引擎原理

前言

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

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

生成抽象语法树

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

词法分析

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

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

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

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

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

语法分析

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

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

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

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

生成字节码

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

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

执行代码

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

即时编译

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

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

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

内联缓存

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

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

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

6-sum.png

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

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

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

7-sum-2.png

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


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

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

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

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

参考链接


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

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

codingonfire.jpg

你可能感兴趣的:(javascript,前端,html5,vue.js,v8)