vuejs设计与实现 15-17 编译器

Vue

1. 编译器核心技术概览

2. 解析器

3. 编译优化


编译器核心技术概览

  1. 模板DSL的编译器
  • 编译器,一段程序,将语言A翻译成语言B;其中语言A叫源代码,语言B叫目标代码;
  • 编译器将源代码翻译为目标代码的过程叫编译;
  • 完整的编译过程包括:词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤;
  • 整个编译流程分为编译前端和编译后端;编译前端包括词法分析、语法分析、语义分析,通常与目标平台无关,仅负责分析源代码;编译后端通常与目标平台有关,涉及中间代码生成、优化、目标代码生成;其中编译后的不一定包含中间代码生成和优化;
  • Vue.js的模板作为DSL,其编译流程有所不同。对于Vue.js模板编译器来说,源代码就是组件的模板,而目标代码是能够在浏览器平台上运行的JavaScript代码,或其他拥有JavaScript运行时的平台代码。Vue.js模板编译器的目标代码其实就是渲染函数。
  • 详细而言,Vue.js模板编译器会首先对模板进行词法分析和语法分析,得到模板AST;接着,将模板AST转换成JavaScript AST;最后根据JavaScript AST生成JavaScript代码,即渲染函数代码。
  • Vue.js模板编译器工作流程:模板 =》词法分析 =》语法分析 =》模板AST =》 Transformer =》JavaScript AST =》代码生成=》得到渲染函数;
  • AST,抽象语法树;模板AST,就是用来描述模板的抽象语法树;AST其实就是一个具有层级结构的对象,模板AST具有与模板同构的嵌套结构;每一棵AST都有一个逻辑上的根节点,其类型为Root;模板中真正的根节点则作为Root节点的children存在;
  • 不同类型的节点是通过节点的type属性进行区分的,如标签节点的type值为‘Element’;
  • 标签节点的子节点存储在其children数组中;
  • 标签阶段的属性阶段和指令节点会存储在props数组中;
  • 不同类型的节点会使用不同的对象属性进行描述,如指令节点拥有name属性用来表达指令的名称,而表达式节点拥有content属性,用来描述表达式的内容;
    1. 可以通过封装parse函数完成对模板的词法分析和语法分析,得到模板AST:const templateAST = parse(template);;有了模板AST后,对其进行语义分析,并对模板AST进行转换;语义分析:如检查v-else指令是否存在相符的v-if指令;分析属性值是否是静态的,是否是常量等;插槽是否会引用上层作用域的变量;在语义分析的基础上,即可得到模板AST;
    1. 接着需要将模板AST转换为JavaScript AST,因为Vue.js模板编译器的最终目标是生成渲染函数,而渲染函数本质上是JavaScript代码,所以需要将模板AST转换成用于描述渲染函数的JavaScript AST;const jsAST = transform(templateAST);
    1. 有了JavaScript AST后,就可以根据它生成渲染函数了;const code = generate(jsAST);generate函数会将渲染函数的代码以字符串的形式返回,并存储在code常量中;
  • Vue.js模板编译为渲染函数的完整流程:模板=》parse(str)=》模板AST=》transform(ast)=》JavaScript AST=》generate(JSAST)=》渲染函数
  • Vue.js模板编译器由三部分组成:(1)用来将模板字符串解析为模板AST的解析器(parser); (2)用来将模板AST转换为JavaScript AST的转换器(transforner);(3)用来就根据JavaScript AST生成渲染函数代码的生成器(generator);
  1. parser的实现原理和状态机
  • parser(字符串模板参数),解析器会逐个读取字符串模板中的字符,并根据一定的规则将整个字符串切割为一个个Token,如<p>Vue</p>会被切割为三个Token:(1)开始标签:

    ,{type: 'tag', name: 'p'}; (2)文本节点:Vue,{type:'text', content: 'Vue'};(3)结束标签:

    ,{type: 'tagEnd', name: 'p'};
  • 有限状态自动机:有限状态指有限个状态(如初始状态、标签开始状态、标签名称状态、文本状态、结束标签状态、结束标签名称状态);自动机指随着字符的输入,解析器会自动地在不同状态间迁移;如分析模板字符串时,parse函数会逐个读取字符,状态机会有一个初始状态,依次读取字符时,状态根据字符情况进行变化;有限状态自动机可以帮助我们完成对模板的标记化,最终得到一系列Token;进而可以用它们构建一棵AST了;
  1. 构造AST
  • 根据Token列表构建AST的过程,其实就是对Token列表进行扫描的过程,从第一个Token开始,顺序地扫描整个Token列表,直到列表中的所有Token处理完毕,在这个过程中,我们需要维护一个栈elementStack,用于维护元素间的父子关系。没遇到一个开始标签阶段,就构造一个Element类型的AST节点,将其压入栈中;每遇到一个结束标签节点,则将当前栈顶的节点弹出,这样栈顶的节点将始终充当父节点的角色。扫描过程中遇到的所有节点,都会作为当前栈顶节点的子节点,并添加到栈顶节点的children属性下。
  1. AST的转换
  • AST转换,对AST进行一系列操作,将其转换为新的AST的过程;新的AST可以是原语言或原DSL的描述,也可以是其他语言或其他DSL的描述;如对模板AST进行操作,将其转换为JavaScript AST,转换后的AST可以用于代码生成,即是Vue.js的模板编译器将模板编译为渲染函数的过程。
  • 模板编译器将模板编译为渲染函数的过程:模板AST=》transform(ast)=》JavaScript AST=》generate(JSAST)=》渲染函数
  1. 将模板AST转为JavaScript AST
  • 为什么要将模板AST转换为JavaScript AST呢?因为我们需要将模板编译为渲染函数,而渲染函数是由JavaScript代码来描述的,因此需要将模板AST转换为用于描述渲染函数的JavaScript AST;
  • JavaScript AST,是JavaScript代码的描述;本质上需要设计一些数据结构来描述渲染函数的代码;渲染函数返回的是虚拟DOM节点,具体体现在h函数的调用;
  1. 代码生成
  • 如何根据JavaScript AST生成渲染函数的代码,即代码生成,代码本质上是字符串拼接的艺术;
  • Vue.js模板编译,用于将模板编译为渲染函数,工作流程:(1)分析模板,将其解析为模板AST;(2)将模板AST转换为用于描述渲染函数的JavaScript AST;(3)根据JavaScript AST生成渲染函数代码;
const templateAST = parse(template);
const jsAST = transform(templateAST);
const code = generate(jsAST);
  • 词法分析的过程就是状态机在不同状态之间迁移的过程。在此过程中,状态机会产生一个Token,形成一个Token列表,使用该Token列表构造用于描述模板的AST。具体做法:扫描Token列表并维护一个开始标签栈,每当扫描到一个开始标签节点,将其压入栈顶;栈顶的节点始终作为下一个扫描节点的父节点;这样,当所有Token扫描完毕后,即可构建出一棵树型AST。
  • AST是树型数据结构,采用深度优先的方式对AST进行遍历,在遍历的过程中,可以对AST节点进行各种操作,从而实现对AST的转换。
  • 模板AST用于描述模板。JavaScript AST用于描述JavaScript代码。只有把模板AST转换为JavaScript AST后,才能据此生成最终的渲染函数代码。
  • 渲染函数代码的生成工作:代码生成是模板编译器的最后一步工作,生成的代码将作为组件的渲染函数。代码生成的过程就是字符串拼接的过程。需要为不同的AST节点编写对应的代码生成函数。

解析器

  • 解析器,本质上是一个状态机;
  • 文本模式指的是解析器在工作时所进入到的一些特殊状态;在不同的特殊状态下,解析器对文本的解析行为会有所不同;当解析器遇到一些特殊标签时,会切换模式,从而影响其对文本的解析行为,如解析器的初始模式是DATA模式,当遇到title标签、textarea标签时,会切换到RCDATA模式;遇到style、xmp、iframe、noembed、noframes、noscript等标签,会切换到RAWTEXT模式;当遇到<![CDATA[字符串时,会切换到CDATA模式
  • 递归下降算法构造模板AST:首先定义了一个状态表TextModes,用来描述预定义的文本模式;然后定义parse函数,即解析器函数,在其中定义了上下文对象context,用来维护解析程序执行过程中程序的各种状态,接着调用parseChildren函数进行解析,该函数会返回解析后得到的子节点,并使用这些子节点作为children来创建Root根节点;最后parse函数返回根节点,完成模板AST的构建;parseChildren函数本质上是一个状态机,该状态机油多少种状态取决于子节点的类型数量,在模板中,子节点可以是以下几种,标签节点、文本插值节点、普通文本节点、注释节点、CDATA节点<![CDATA[xxx]]>;会开启一个while循环使得状态机自动运行;
  • 当解析器遇到开始标签时,会将该标签压入父级节点栈,同时开启新的状态机;当解析器遇到结束标签,且父级节点栈中存在与该标签同名的开始标签节点时,会停止当前正在运行的状态机;
  • 状态机停止的时机:1.当模板内容被解析完毕时;2.在遇到结束标签时,这时解析器会取得父级节点栈栈顶的节点作为父节点,检查该结束标签是否与父节点的标签同名,如果相同则状态机停止运行;
  • 一个完整的标签元素由开始标签、子节点、结束标签构成;在parseChildren中,分别调用三个解析函数来处理这三部分内容,parseTag解析开始标签,递归调用parseChildren解析子节点,parseEndTag处理结束标签;
  • 解析属性parseAttributes;parseText解析文本;parseComment解析注释;parseInterpolation解析插值;

编译优化

  • 编译优化指的是编译器将模板编译为渲染函数的过程中,尽可能地提取关键信息,并以此指导生成最优代码的过程;尽可能的区分动态内容和静态内容,并针对不同的内容采用不同的优化策略;
  • vue.js3的编译器会将编译时得到的关键信息附着在它生成的虚拟DOM上,这些信息会通过虚拟DOM传递给渲染器,渲染器会根据这些关键信息执行快捷路径,从而提升运行时的性能。
  • 编译优化的核心在于,区分动态节点和静态节点。vue.js3会为动态节点打上补丁标志,即patchFlag;同时Vue.js3还提出Block概念,一个Block本质上是一个虚拟节点,与普通虚拟节点相比,多出一个dynamicChildren数组;该数组用来收集所有动态子代节点,利用了createVNode函数和createBlock函数的层层嵌套调用的特点,即以由内向外的方式执行,再配合一个用来临时存储动态节点的节点栈,即可完成动态子代节点的收集;v-if、v-for会影响DOM层级结构的稳定性,让带有v-if、v-for的节点也作为Block角色即可;
  • 除了Block、打补丁之外,vue.js3在编译方面还做了其他努力:静态提升,能够减少更新时创建虚拟DOM带来的性能开销和内存占用;预字符串化,在静态提升的基础上,对静态节点进行字符串化,能减少创建虚拟节点产生的性能开销和内存占用;缓存内联事件处理函数,避免不必要的组件更新;v-once指令,缓存全部或部分虚拟节点,能够避免组件更新时重新创建虚拟DOM带来的性能开销,也可避免无用的Diff操作。






参考&感谢各路大神

1. vue.js设计与实现-霍春阳

posted @ 2024-07-24 22:21  安静的嘶吼  阅读(5)  评论(0编辑  收藏  举报