至今,在浏览器里,我们可以运行很多高度复杂的应用,比如办公套件等,这些都得益于以 V8 为代表的 JavaScript 引擎的进步。对 JavaScript 编译器来说,它最大的挑战就在于,当我们打开一个页面的时候,源代码的下载、解析(Parse)、编译(Compile)和执行,都要在很短的时间内完成,否则就会影响到用户的体验。

V8 的编译器的构成跟 Java 的编译器很像,它们都有从源代码编译到字节码的编译器,也都有解释器(叫 Ignition),也都有 JIT 编译器(叫 TurboFan)。此外,在 V8 的编译过程中,有两个比较有特色的处理阶段:流处理结点(Stream)和预解析器(Preparser)。

ce80ee2ace64988a8d332e0e545ef0ee.webp

095613e2515fc6d6cc36705e6d952afb.webp

V8 解析源代码的速度必须要非常快才行,源代码边下载边解析完毕,在这个过程中,用户几乎感觉不到停顿。那它是如何实现的呢?

第 1 个原因,V8 的整个解析过程是流(Stream)化的,也就是一边从网络下载源代码,一边解析。在下载后,各种不同的编码还被统一转化为 UTF-16 编码单位,词法解析器不需要处理多种编码。

第 2 个原因,是识别标识符时所做的优化,标识符的第 1 个字符(ID_START)只允许用字母、下划线和 $ 来表示,而之后的字符(ID_CONTINUE)还可以包括数字。所以,当词法分析器遇到一个字符的时候,首先判断它是否是合法的 ID_START。

if(ch >= 'A' && ch <= 'Z' || ch >='a' && ch<='z' || ch == '$' || ch == '_'){
    return true;
}

最坏情况下,要做 6 次比较运算和 3 次逻辑 “或” 运算。V8 通过查表的方法,来识别每个 ASCII 字符是否是合法的标识符开头字符。

这相当于准备了一张大表,每个字符在里面对应一个位置,标明了该字符是否是合法的标识符开头字符。这是典型的牺牲空间来换效率的方法。虽然在阅读代码的时候,会发现它调用了几层函数来实现这个功能,但这些函数其实是内联的,并且在编译优化以后,产生的指令要少很多,所以这个方法的性能更高。