浅析vue模板编译流程

31 阅读3分钟

什么是编译?

编译器将一段源代码转换成目标代码的过程叫作编译,也就是将“语言A”=>“语言B”的过程。

编译流程

一个完整的编译过程通常包括以下几个步骤:

  1. 词法分析
  2. 语法分析
  3. 中间代码生成
  4. 优化
  5. 目标代码生成

VUE模板编译器在编译转换过程中都做了哪些事呢?

与上述编译流程中不同的是,在vue的模板编译流程中,充当“源代码”角色的是组件模板,而“目标代码”则是一个JS渲染函数。

    // 源代码
    <div>
        <p id='myApp' >source code</p>
    </div>
    
    //目标代码
    function render(){
        return  h('div',[
            h('p', { id: 'myApp', 'source code'})
        ])
    }

编译器将模板转换成render函数,这个过程中编译器会对模板字符串进行词法分析和语法分析,得到模板AST,将模板AST转换成JavaScript AST最后生成渲染函数render()。

AST是什么?

抽象语法树(abstract syntax tree,AST)  是一种源代码抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,它并不依赖源代码的语法。

定义一个模板AST结构如下:

    // 模板
    <div>
        <p id='myApp'>source code</p>
    </div>
    
    // 模板AST
    const myAST = {
        type: 'Root',
        children: [
            // div 标签节点
            {
                type: 'Element',
                tag: 'div',
                children: [
                    // p 标签节点
                    {
                        type: 'Element',
                        tag: 'p',
                        props: [
                            //属性节点
                            {
                                type: 'attrs',
                                name: 'id',
                                content: 'myApp'
                            }
                        ],
                        children: [
                            // 文本节点
                            {
                                type: 'text',
                                content: 'source code'
                            }
                        ]
                    }
                ]
            }
        ]
    }

以上的代码中我们可以看到

  1. 模板AST通常用不同的type属性来区分
  2. 标签节点的子节点存储在children数组中
  3. 节点上的属性存储在props数组中

那么我们是如何将一段模板代码转换成AST的呢?

想要得到模板AST并对其进行转换,需要经过编译流程中的词法分析和语法分析。要想实现这一过程,我们需要封装一个parser函数,在该函数中我们将会把模板字符串切割得到一个个token,再将token数组遍历构建AST树。

    function parser(template){
        //对模板字符串进行切割划分
        const tokens = tokenInit(template)
        // const tokens = [
        //     {type: "tag", name: "div"}, //div 开始标签
        //     {type: "tag", name: "p"}, //p 开始标签
        //     {type: "text", content: "source code"}, // 文件节点
        //     {type: "tagEnd", name: "p"}, // p 结束标签
        //     {type: "tagEnd", name: "div"} // div 结束标签
        // ]
        //创建根节点
        const root = [
            type: 'Root',
            parent: null,
            children: []
        ]
        //创建一个栈,充当tokens和AST的中转容器
        //将tokens数组中的元素挨个压入栈中,把栈顶元素当做父节点,
        //对类型为‘tag’的节点作为子节点压入children中,遇到类型为‘tagEnd’就弹出当前栈顶元素,
        //最后返回root根节点作为AST
        const elementStack = [root]
        
        while(tokens.length){
            //elementStack栈的长度会不断改变,所以要动态的获取栈顶元素作为当前节点
            const currentNode = elementStack[elementStack.length -1]; 
            const currentToken = tokens[0]
            switch (currentToken.type){
                case 'tag':
                    //创建AST节点
                    const element = {
                        type: 'Element',
                        tag: currentToken.name,
                        children: []
                    }
                    //将节点压入当前栈顶元素的children中。如果是第一个节点,将当前节点压入root.children中
                    currentNode.children.push(element)
                    elementStack.push(currentNode)
                break;
                case 'text':
                    const test = {
                        type: 'Text',
                        content: currentNode.content
                    }
                    currentNode.children.push(text)
                break;
                case 'tagEnd':
                    //弹出当前节点
                    elementStack.pop()
                break;
            }
            tokens.shift(); // 弹出当前token
        }
        //返回根节点AST,遍历结束后根节点中已经包含了全部节点
        return root;
    }

将模板AST转换成JavaScript AST

与上述的模板AST类似,我们将一个函数拆分,会发现一个函数可以分为以下几种类型:

  1. 函数名称
  2. 函数参数
  3. 函数体

JavaScript AST与模板 AST没有太大区别

function render(){
    return  h('div',[
        h('p', { id: 'myApp', 'source code'})
    ])
}
// JavaScript AST结构
const FunctionNode = {
    // 函数节点类型
    type: 'Function',
    id: {
        type: 'Identifier',
        //函数名称
        name: 'render'
    },
    //函数的参数
    params: [],
    //函数体
    body: [
        {
            type: 'ReturnStatement',
            return: {
                //回调函数类型
                type: 'CallEXP',
                callee: {
                    type: 'Identifier',
                    name: 'h'
                },
                //arguments参数
                arguments: [
                    {
                        //字符串类型
                        type: 'StringEXP',
                        value: 'div'
                    },
                    {
                        //数组类型
                        type: 'ArrayEXP',
                        elements: [
                            {
                                type: 'CallEXP',
                                callee: {
                                    type: 'Identifier',
                                    name: 'h'
                                },
                                arguments: [
                                    {
                                        type: 'StringEXP',
                                        value: 'p'
                                    }
                                    ...
                                ]
                            }
                            
                        ],
                        
                    }
                ]
            }
        }
    ]
}

Vue编译器通过将JavaScript AST节点遍历递归解析转换成渲染函数。在这个代码生成的过程,其本质就是字符串拼接,具体不再做讨论。

参考

  • Vue.js设计与实现