vue模板解析原理

1,175 阅读5分钟

在进行日常的vue项目开发时,我们采用的时官方推荐的SFC(single file component)规范,将我们<template>标签中写入我们需要渲染的视图模板,vue会根据组件内的业务逻辑生成最终的实际视图。模板中的内容跟我们日常的html文档节点类似,但是vue的模板中还拥有静态html不具备的能力,模板内容中除了静态的元素节点,还包含属性、指令、插槽、组件、表达式等更丰富的内容。从简单字符串模板到最终的实际dom,这中间经历了哪些过程?

1、模板解析

为了开发者开发的便利,vue提供了template的模板语法。对于开发者来说,这种结构近于真实的dom结构,利于视图的构建。但是对于vue程序来说,他们此时还只是一堆有特殊意义的字符串而已(只是文本描述),如果需要使用的话还需要进行进一步的处理(得到js对象的描述)。因为在vue生成实际的视图时,需要了解到视图内部的状态、视图中的属性、组件、静态节点、指令等内容。所以我们需要让vue知道vue关心的内容。
vue的模板解析是通过正则捕获关键数据+字符串的截取,边解析边截取,并最终生成一个可以描述整个模板的AST抽象语法树实现的。 假定模板如下:

<div class="test" :prop1="vProp1" @click="clickHandler" v-dir="d">
    <span>text</span>
    <component-a></component-a>
</div>

这里为了能完整描述模板的树型层级结构,需要使用一个栈型结构来记录匹配记录,保证每个节点的都能正确获取到元素的父子层级关系。栈中最后一个元素节点将被标记为当前父元素,接下来开始模板解析......

说明:
以下'删除'代指删除模板字符串内对应的字符串
栈中最后一个元素会被标记为当前父元素

1、首先通过正则捕获模板的开始标签('<div'),记录开始标签的元素名称(首个开始元素会被标记为根节点,'div'进栈)。'删除<div'。然后循环匹配标签上的属性(指令、事件),匹配到一个属性就记录一个属性的信息并'删除对应属性',直到匹配到开始标签的结束标识符('>')。匹配到开始标签的结束标识符后直接删除开始标签的结束标签('>')。
2、匹配span的开始标签('<span',span当前的父元素是节点是div,span进栈),'删除<span',匹配span的结束标签('>'),'删除>'。匹配文本('text',文本节点的父亲元素是span),'删除text',匹配span的元素结束标签('</span>'),'删除</span>'。span元素弹出(栈),这时当前父元素又变成div。
3、匹配component-a的开始标签('component-a',component-a的当前父元素是div,component-a进栈),'删除<component-a'。匹配component-a开始标签的结束符('>'),'删除>',匹配component-a的结束标签('</component-a>'),'删除</component-a>',component-a'元素'弹出(栈)。
4、匹配div开始标签的结束标签('</div>'),'删除</div>'。div弹出(栈),模板内容截取剩余为空。模板解析完毕。

通过上面的一系列流程后,我们得到一个类似于下面这种AST对象:

{
  ast: {
    type: 1,
    tag: 'div',
    attrsList: [ 
      { name: ':prop1', value: 'vProp1' },
      { name: '@click', value: 'clickHandler' },
      { name: 'v-dir', value: 'd' }
    ],
    children: [
         {
            type: 1,
            tag: 'span',
            attrsList: [],
            parent: {
              type: 1,
              tag: 'div',
              attrsList: [Array],
              parent: undefined,
              children: [Circular *1],
              attrs: [Array],
              events: [Object],
              directives: [Array],
            },
            children: [
                { type: 3, text: 'text' }
            ]
       },
       {
        type: 1,
        tag: 'component-a',
        attrsList: [],
        parent: {
          type: 1,
          tag: 'div',
          attrsList: [Array],
          parent: undefined,
          children: [Circular *1],
          events: [Object],
          directives: [Array]
        },
        children: [],
      }
    ],
    attrs: [ [Object] ],
    events: { click: [Object] },
    directives: [ [Object] ],
  }
}

2、代码生成

得到ast语法树后,vue默认会进行一些优化。这里先忽略......
可以看到ast语法树上有标识当前元素的标签名、类型、属性、子元素、父元素。接下来该使用这些信息了。 如果只是静态的元素节点,当元素的标签、类型、属性明确后,这就足够去创建一个真实的元素了。如果标签名是原生的html标签,就创建原生的元素节点;如果是文本节点,就可以创建文本节点插入到指定位置。
这种定式的逻辑,我们肯定会使用函数去处理。vue中会使用codegen将整个ast语法树转化成一个可以创建视图的函数。codegen函数内部会遍历整个ast语法树,通过字符串拼接+ with + new Function的方式生成一个render函数,所以一般的视图渲染函数中会看到许多_c、_v、_t这种方法,它们实际的作用就是创建对应的节点。with方法中会传入当前视图的实例,所以我们可以省略this。执行render函数后可以生成对应模板的视图。
上面的ast最终会得到下面这种结果。

with(this){
    return _c('div',{
                        directives:[
                                    {name:"dir",rawName:"v-dir",value:(d),expression:"d"}
                                   ],
                        staticClass:"test",
                        attrs:{"prop1":vProp1},
                        on:{"click":clickHandler}
                     },
                     [_c('span',[_v("text")]),_v(" "),_c('component-a')],1)},1)

3、总结

最后总结一下:vue的模板解析是有:
1、字符模板-> 2、ast抽象语法树 -> 3、render函数 这3个步骤。使用webpack时,webpack会使用vue-loader和vue-tempalte-copmpiler事先将模板先编译成render函数。 当然具体的内部实现还会更加复杂,但是大致实现都是差不多的。

在生成抽象语法树的过程中需要注意的是:
1、模板解析的工具是正则
2、模板解析的顺序从开始标签->属性->开始标签结束符->子节点->标签结束标签
在生成render函数需要注意的是:
1、函数是通过字符串拼接 + new Function + with 实现的
2、特殊属性的处理:如指令、事件、动态属性、class、style
3、插槽的处理
4、以_开头的方法实际上是创建各种节点