V8是如何快速地解析JavaScript延迟解析
实际上,忽略变量声明和顶层函数的引用是不正确的。ECMAScript规范要求在第一次解析脚本时要检测各种类型的变量冲突。例如,如果一个变量在同一作用域内被两次声明为词法变量,则被认为是early SyntaxError。因为我们的预解析器只是跳过了变量声明,所以在预解析过程中它将允许代码错误地运行。此时我们认为性能上的胜利使对规范的违反情有可原。现在预解析器 能正确地跟踪变量,尽管如此,我们还是应该在没有明显性能代价的情况下消除这类与变量解析相关的违反规范的行为。 跳过内部函数 如前所述,当第一次调用一个预解析的函数时,我们将对其进行完全解析,并将生成的AST编译为字节码。 ![]() 该函数直接指向外部上下文,其中包含内部函数需要使用的变量声明的值。为了允许函数的延迟编译(并支持调试器),上下文会指向一个名为ScopeInfo的元数据对象。ScopeInfo对象描述了上下文中列出的变量。这意味着在编译内部函数时,我们可以计算变量在上下文链中的位置。 但是,要计算延迟编译的函数本身是否需要上下文,我们需要再次执行范围解析: 我们需要知道嵌套在延迟编译的函数中的函数是否引用了由延迟函数声明的变量。我们可以通过重新解析这些函数来计算出来。这正是V8在升级到V8v6.3/Chrome63之前所做的。但是,这并不是理想的性能最优的方法,因为它使资源大小和解析成本之间的关系变成非线性: 我们将尽可能多地解析嵌套函数。除了动态程序的自然嵌套之外,JavaScript打包器通常用“即时调用函数表达式”(IIFEs)的方式来包装代码,这使得大多数JavaScript程序具有多个嵌套层。 ![]() 每次重新解析至少会增加解析函数的成本。 为了避免非线性性能开销,我们甚至在预解析过程中执行全作用域解析。我们存储了足够的元数据,这样我们稍后就可以简单地跳过内部函数,而不必重新解析它们。一种方法是存储由内部函数引用的变量名。这样做的存储成本很高,并要求我们仍然进行重复工作:我们已经在预解析期间执行了变量解析。 相反,我们将在变量分配的地方将每一个变量序列化为它的一个密集标记数组。当我们延迟解析一个函数时,变量按照预解析器看到的顺序被重新创建,我们可以简单地将元数据应用于这些变量。现在函数已经编译完成,已经不再需要变量分配元数据了,这样它就可以被当做垃圾进行回收。由于我们只需要这个元数据来处理实际包含内部函数的函数,所以大部分函数甚至不需要这个元数据,从而显著地降低了内存开销。 ![]() 通过跟踪预解析的函数的元数据,我们可以完全跳过内部函数。 (编辑:西安站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |