乐趣区

关于vue3:图解vue30编译器核心原理

概览

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

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

当初重点采纳图解步骤剖析一下编译器的简略工作原理;

编译器概念

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

要害概念

次要波及的概念:

  • DSL 畛域特定语言
  • AST 形象语法树(Abstract Syntax Tree)
  • 无限状态机
  • 深度优先算法

简略流程

一个规范的编译器流程如下图所示:

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

Vue 的编译器

Vue.js的指标代码其实就是渲染函数(render函数)。详情而言,Vue.js编译器首先对模板进行词法剖析、语法分析,而后失去模板的形象语法树(AST)。随后将模板 AST 转换成 JavaScript AST,最初再转换成JavaScript 代码,及渲染函数。一个简略的 Vue.js 模板编译器的工作流如下:

简略如下:
模板代码

<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 数组中。
  • 不同类型的节点会应用不同的对象属性进行形容。

编译过程

parse 函数

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

const template = `
    <div>
        <h1>vue<h1>
    </div>
`
const templateAst = parse(template)

解析器是如何对模板字符串进行宰割的呢,此处就须要用到无限状态自动机。指的是在无限个状态之间,随着字符的输出,解析器会主动地在不同的状态之间进行切换。(实际上无限状态机是能够应用正则表达式来实现的)。

简略的状态机流程图:

通过无限状态机原理,能够帮忙咱们实现对模板的标记,最终将失去一系列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 函数仅做了简要阐明,具体实现原理敬请期待;

transform 函数

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

generate 函数

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

残缺流程

以上就是 Vue 模板编译器的根本构造和工作流程,它次要有三个局部组成:

  • 用来将模板字符串解析为模板 AST 的解析器(parser);
  • 用来将模板 AST 解析成 JavaScript AST 的转换器(transformer);
  • 用来依据 JavaScript AST 生成渲染函数代码的生成器(generator);

本文章次要探讨了 parser 的根本实现原理(实际上 Vue.js 的真正实现要简单的多,比方正则解析、Vue 语法解析 v -if、v-show、内插值 {{}} 等等),以及如何应用无限状态自动机来结构一个词法分析器,其过程就是状态机在不同的状态之间进行迁徙的过程,并生成一个 Token 列表汇合。而后应用 Token 列表汇合和顶节点元素栈来结构一个能够用来形容模板的 AST,最初应用模板 AST 来解析成 JavaScript AST 和渲染函数。

作者:GFE- 绝对零度
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

参考

Vue.js 源码;

Vue.js 设计与实现;

退出移动版