写点什么

图解 vue3.0 编译器核心原理

作者:GFE
  • 2022-11-03
    北京
  • 本文字数:4166 字

    阅读完需:约 14 分钟

图解vue3.0编译器核心原理

概览

Vue.js 作为目前最流行的前端框架之一,一些概念和原理还是需要我们前端开发人员了解与深入理解的。


Vue.js 涉及的知识点很多,一些重要概念,例如:如何使用proxy实现响应式effect,虚拟DOMDiff算法及演变过程(双端 Diff 算法、快速 Diff 算法等),渲染器原理的实现,编译器、解析器的工作原理,动态节点、静态提升等等;


现在重点采用图解步骤分析一下编译器的简单工作原理;


<a name="dc0842d1"></a>

编译器概念

编译器其实就是一段JavaScript代码程序,它将一种语言(A)编译成另外一种语言(B),其中前者A通常被叫做源代码,后者B通常被叫做为目标代码。例如我们 vue 的前端项目的.vue文件一般即为源代码,而编译后dist文件里的.js文件即为目标代码;这个过程就被称为编译(compile


<a name="3310cc3f"></a>

关键概念

主要涉及的概念:


  • DSL 领域特定语言

  • AST 抽象语法树(Abstract Syntax Tree)

  • 有限状态机

  • 深度优先算法


<a name="e9006bf0"></a>

简单流程

一个标准的编译器流程如下图所示:<br />



<br />Vue.js作为DSL,其编译流程会与上图有所不同,对于Vue.js来说,源代码就是组件的模板代码,而目标代码就是能够在浏览器(或其他平台)平台上运行的JavaScript代码。


<a name="40ce000d"></a>

Vue 的编译器

Vue.js的目标代码其实就是渲染函数(render函数)。概况而言,Vue.js编译器首先对模板进行词法分析、语法分析,然后得到模板的抽象语法树(AST)。随后将模板AST转换成JavaScript AST,最后再转换成JavaScript代码,及渲染函数。一个简单的 Vue.js 模板编译器的工作流如下:<br />



简单如下:<br />模板代码


<div>    <h1 id="vue">vue_compiler</h1></div>
复制代码


目标的 AST


const ast = {    type: 'Root',    children: [        {            type: 'Element',            tag: 'div',            children: [                {                    type:'Element',                    tag: 'h1',                    props: [                        {                            type: 'Attribute',                            name: 'id',                            content: 'vue'                        }                    ],                    children: [                        {                            type: 'Text',                            content: 'vue_compiler'                        }                    ]                }            ]        }    ]}
复制代码


目标代码


function render() {    return h('div', [        h('h1', {id: 'vue'}, 'vue_compiler')    ])}
复制代码


由以上代码可以看出,AST其实就是一个具有层级结构的对象,模板的AST与模板具有相同的嵌套结构。每一颗AST都有一个逻辑上的根节点,其类型为Root,而模板中真正的根节点则作为Root节点的children存在。


观察AST可知:


  • 不同类型的节点是通过节点的type属性进行区分的。

  • 标签节点的子节点存储在其children数组中。

  • 标签节点的属性节点会存储在props数组中。

  • 不同类型的节点会使用不同的对象属性进行描述。


<a name="bde58c19"></a>

编译过程

<a name="35530dc0"></a>

parse 函数

Vue.js通过封装parse函数,实现对模板的词法分析和语法分析,最终得到模板的 AST。parse函数接收模板字符串作为参数,并将解析后的AST作为返回值返回;


const template = `    <div>        <h1>vue<h1>    </div>`const templateAst = parse(template)
复制代码



解析器是如何对模板字符串进行分割的呢,此处就需要用到有限状态自动机。指的是在有限个状态之间,随着字符的输入,解析器会自动地在不同的状态之间进行切换。(实际上有限状态机是可以使用正则表达式来实现的)。


简单的状态机流程图:<br />



<br />通过有限状态机原理,可以帮助我们完成对模板的标记,最终将得到一系列Token(词法标记号)。


假设有如下代码:


const template = `<div><span>Vue</span><p>Vue Compiler</p></div>` // 模板字符串
// 通过有限状态机原理实现词法分解得到三个Token// 开始标签 <div>// 文本节点 vue// 结束标签 </div>
// 最终值为const tokens = tokenize(template);// [// {// type: 'tag', name: 'div'// },// {// type: 'tag', name: 'span'// },// {// type: 'text', name: 'Vue'// },// {// type: 'tagEnd', name: 'span'// },// {// type: 'tag', name: 'p'// },// {// type: 'text', name: 'Vue Compiler'// },// {// type: 'tagEnd', name: 'p'// },// {// type: 'tagEnd', name: 'div'// }// ]

// 此代码需要生成的AST应为const ast = { type: 'Root', children: [ { // 实际的根节点 type: 'Element', tag:: 'div', children: [ { type: 'Element', tag:: 'span', children: [ { type: 'Text', content: 'Vue' } ] }, { type: 'Element', tag:: 'p', children: [ { type: 'Text', content: 'Vue Compiler' } ] } ] } ]}
复制代码


以上代码生成的 AST 数据结构 HTML 结构相同,都是树状结构



接下来要做的就是将生成的tokens转换成AST,在转换过程中需要维护一个Stack,这个栈将用来维护元素间的父子关系。每到遇到一个开始标签,就创建一个Element类型的 AST 节点,并将其压入栈内,类似的,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。这样栈顶的节点将始终充当父节点的角色。转换过程中的所有节点,都将作为当前栈顶节点的子节点,并添加到栈顶节点的children属性下。流程如下图示:


最初节点只有根节点 Root



当扫描到第一个标签是开始节点时,因此我们创建一个类型为Element的 AST 节点Element(div),并将该节点作为当前节点的子节点。由于当前的栈顶节点是Root节点,所以新创建的Element(div)节点作为Root节点的子节点被添加到AST中,最后将新建的Element(div)节点压入栈中。



由于第二个节点也是一个开始标签,所以流程同上一步,只不过当前的栈顶节点为Element(div),所以将当前的节点Element(span)作为其子节点添加到AST中,最后将Element(div)节点压入栈中。



接下来的节点是一个文本节点,所以需要创建一个Text类型的AST节点,并将其作为栈顶节点Element(span)的子节点加入到 AST 中,不同的时,当前接待不是Element类型,所以不需要压入栈中;



下面是一个结束标签节点,根据规则,则需要将当前栈顶的节点弹出。



后面的流程此处就不再累述



最终完成后的效果如下:



现在我们来实现 parse 函数


function parse(str) {    // 对模板进行词法分析,得到节点list    const tokens = okenize(template);    // 创建跟节点    const root = {        type: 'Root',        children: []    };    // 创建节点栈,root节点作为栈的根节点    const stack = [root];    while(tokens.length) {        const parent = stack[stack.length - 1];        const token = tokens[0] // 从第一个点开始        switch(t.type) {            case 'tag':                const eleNode = {                    type: 'Element',                    tag: t.name,                    children: []                }                parent.children.push(eleNode);                stack.push(eleNode);                break;            case 'text':                const textNode = {                    type: 'Text',                    content: t.content                }                parent.children.push(textNode);                break;            case 'tagEnd':                // 结束标签,将栈顶节点弹出栈                stack.pop();                break;        }        // 消费掉已处理的节点        tokens.shift()    }    return root}
复制代码


以上就是一个简版的 parse 函数的实现,当然相对于 Vue.js 的源码还有很多差异,但基本原理大致相同。


下面关于transform函数和generate函数仅做了简要说明,具体实现原理敬请期待;


<a name="30a21103"></a>

transform 函数

const template = `    <div>        <h1>vue<h1>    </div>`const templateAst = parse(template)const jsAst = transform(templateAst)
复制代码



<a name="5c489d8a"></a>

generate 函数

const template = `    <div>        <h1>vue<h1>    </div>`const templateAst = parse(template)const jsAst = transform(templateAst)const code = generate(jsAst)
复制代码



<a name="10476cf7"></a>

完整流程

以上就是Vue模板编译器的基本结构和工作流程,它主要有三个部分组成:


  • 用来将模板字符串解析为模板AST的解析器(parser);

  • 用来将模板AST解析成JavaScript AST的转换器(transformer);

  • 用来根据JavaScript AST生成渲染函数代码的生成器(generator);


本文章主要讨论了 parser 的基本实现原理(实际上 Vue.js 的真正实现要复杂的多,比如正则解析、Vue 语法解析 v-if、v-show、内插值{{}}等等),以及如何使用有限状态自动机来构造一个词法分析器,其过程就是状态机在不同的状态之间进行迁移的过程,并生成一个 Token 列表集合。然后使用 Token 列表集合和顶节点元素栈来构造一个可以用来描述模板的 AST,最后使用模板 AST 来解析成 JavaScript AST 和渲染函数。



<a name="LgXXN"></a>


作者:GFE-绝对零度<br />著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。<a name="d17a0f0b"></a>

参考

Vue.js 源码;


Vue.js 设计与实现;

用户头像

GFE

关注

一起成长、一起分享、一起创造价值 2020-04-09 加入

💻 努力打造这块领土,构建完整良好的知识体系 🏠 保持对技术的热爱,保持初心,保持丰富的想象力 🏷️ 标签 | 热爱技术 钻研探索 有责任心 前端领域 📥 邮箱 | gfe@goldentec.com

评论

发布
暂无评论
图解vue3.0编译器核心原理_前端_GFE_InfoQ写作社区