认识模板编译
在Vue文件中使用<template></template> 表示模板,其内部包裹的代码并不是原生的HTML,因此浏览器是不认识模板的。所以我们要做的工作就是把<template></template>内部的代码编译成浏览器认识的原生HTML,这就是模板编译。
页面从<template></template>包裹的代码到视图最终展示的主要流程是
- 提取出模板中的原生HTML和非原生HTML(比如绑定的属性、事件、指令等)
 
- 经过一些处理生成render函数
 
- render函数再将模板内容生成对应的VNode
 
- 再经过patch过程(Diff)得到要渲染到视图的VNode
 
- 最后根据VNode创建真实DOM节点,也就是原生的HTML插入到视图中,完成渲染
 
上述的1,2,3步骤就是模板编译的过程。
那代码究竟在步骤2中发生了什么?它是怎么编译,最终生成render函数的呢?
模板编译详解 - 源码
baseCompile()
这个就是模板编译的入口函数,接收两个参数:1.template:就是要转换的模板字符串;2.options:就是转换时需要的参数
编译的流程主要有三步:
- 模板解析:通过正则等方式提取出
<template></template>里的标签元素、属性、变量等信息,并解析成抽象语法树AST 
- 优化:遍历AST找出其中的静态节点和静态根节点,并进行标记
 
- 代码生成:根据AST生成渲染函数render
 
上述三个步骤分别对应三个函数。
首先查看baseCompile()函数在哪里调用,即模板编译从那里开始。
源码地址:src/complier/index.js - 11行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | export const createCompiler = createCompilerCreator(function baseCompile (   template: string,    options: CompilerOptions  ): CompiledResult {      const ast = parse(template.trim(), options)
       if (options.optimize !== false) {          optimize(ast, options)   }      const code = generate(ast, options)   return {     ast,     render: code.render,      staticRenderFns: code.staticRenderFns   } })
   | 
 
上述的3个步骤在源码中均体现出来,接下来看一下具体的编译流程是啥样的。
以以下模板为例:
1 2 3
   | <template>     <div id="app">{{name}}</div> </template>
   | 
 
打印编译后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | {   ast: {     type: 1,     tag: 'div',     attrsList: [ { name: 'id', value: 'app' } ],     attrsMap: { id: 'app' },     rawAttrsMap: {},     parent: undefined,     children: [       {         type: 2,         expression: '_s(name)',         tokens: [ { '@binding': 'name' } ],         text: '{{name}}',         static: false       }     ],     plain: false,     attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],     static: false,     staticRoot: false   },   render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,   staticRenderFns: [],   errors: [],   tips: [] }
  | 
 
生成的内容看起来有些吃力,实际上注意以下三个步骤都做了什么就行了:
- ast字段是第一步生成的
 
- static字段,就是标记,是在第二步中根据ast里的type加上去的
 
- render字段,就是第三步生成的
 
再来看源码。
1.parse()

实现的功能是 通过正则等方法提取出来<template></template>模板字符串里所有的tag、props、children信息,生成一个对应结构的ast对象 。
parse()接收两个参数:
- template:就是要转换的模板字符串
 
- options:就是转换时需要的参数。它包含有四个钩子函数,就是用来把parseHTML解析出来的字符串提取出来,并生成对应的AST
 
核心步骤如下:
调用parseHTML函数对模板字符串进行解析:
- 解析到开始标签、结束标签、文本、注释分别进行不同的处理
 
- 解析过程中遇到文本信息就调用文本解析器
parseText函数进行文本解析 
- 解析过程中遇到包含过滤器,就调用过滤器解析器
parseFilters函数进行解析 
每一步解析的结果都合并到一个对象上(就是最后的AST)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
   |  export function parse (   template: string,    options: CompilerOptions  ): ASTElement | void {   parseHTML(template, {     warn,     expectHTML: options.expectHTML,     isUnaryTag: options.isUnaryTag,     canBeLeftOpenTag: options.canBeLeftOpenTag,     shouldDecodeNewlines: options.shouldDecodeNewlines,     shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,     shouldKeepComment: options.comments,     outputSourceRange: options.outputSourceRange,          start (tag, attrs, unary, start, end) {                  ...     },          end (tag, start, end) {         ...     },          chars (text: string, start: number, end: number) {                     ...     },          comment (text: string, start, end) {              const comment = /^<!\--/       if (comment.test(html)) {              const commentEnd = html.indexOf('-->')       ...     }   })      return root }
 
  | 
 
上面解析文本时调用chars()会根据不同类型的节点加上不同type,来标记AST节点类型,这个属性在下一步标记的时候也会用的
| type | 
AST 节点类型 | 
| 1 | 
元素节点 | 
| 2 | 
包含变量的动态文本节点 | 
| 3 | 
没有变量的纯文本节点 | 
2.optimize()
这个函数就是在AST里找出静态节点和静态根节点,并添加标记,为了后面patch过程中,直接跳过静态节点的比对,直接克隆一份过去,从而优化patch的性能。
optimize()的大致过程:
1 2 3 4 5 6 7 8 9
   | export function optimize (root: ?ASTElement, options: CompilerOptions) {   if (!root) return   isStaticKey = genStaticKeysCached(options.staticKeys || '')   isPlatformReservedTag = options.isReservedTag || no      markStatic(root)      markStaticRoots(root, false) }
  | 
 
3.generate()
这个就是生成render的函数,就是说最终会返回下面这个东东
1 2 3 4 5 6 7
   |  <template>     <div id="app">{{name}}</div> </template>
 
  render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`
 
  | 
 
可以看出上面的render正是虚拟DOM的结构,就是把一个标签分为tag、props、children。
1 2 3 4 5 6 7 8
   |  with(this){   return _c(     'div',     { attrs:{"id":"app"} },     [  _v(_s(name))  ]   ) }
 
  | 
 
其中with是用来欺骗词法作用域的,它可以让我们更快的饮用一个对象上的多个属性

函数内部的_c、_v、_s分别表示什么含义?
1 2 3 4 5 6 7 8 9
   |  export function installRenderHelpers (target: any) {   target._s = toString    target._l = renderList    target._v = createTextVNode    target._e = createEmptyVNode  }
  _c = createElement 
 
  | 
 
继续看generator()的源码
1 2 3 4 5 6 7 8 9 10 11 12 13
   | export function generate (   ast: ASTElement | void,   options: CompilerOptions ): CodegenResult {   const state = new CodegenState(options)      const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
    return {     render: `with(this){return ${code}}`,     staticRenderFns: state.staticRenderFns   } }
   | 
 
这个流程很简单,就是判断AST是不是为空,不为空就根据AST创建VNode,否则就创建一个空的div的VNode。
创建VNode主要是通过genElement()函数来实现的。
4.genElement()
这个函数的逻辑很清晰,通过if/else判断传进来的AST元素节点的属性,来执行不同的生成函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
   | export function genElement (el: ASTElement, state: CodegenState): string {   if (el.parent) {     el.pre = el.pre || el.parent.pre   }
    if (el.staticRoot && !el.staticProcessed) {     return genStatic(el, state)   } else if (el.once && !el.onceProcessed) {      return genOnce(el, state)   } else if (el.for && !el.forProcessed) {      return genFor(el, state)   } else if (el.if && !el.ifProcessed) {      return genIf(el, state)
         } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {     return genChildren(el, state) || 'void 0'   } else if (el.tag === 'slot') {      return genSlot(el, state)   } else {          let code          if (el.component) {       code = genComponent(el.component, el, state)     } else {       let data              if (!el.plain || (el.pre && state.maybeComponent(el))) {         data = genData(el, state)       }              const children = el.inlineTemplate ? null : genChildren(el, state, true)       code = `_c('${el.tag}'${         data ? `,${data}` : '' // data       }${         children ? `,${children}` : '' // children       })`     }          for (let i = 0; i < state.transforms.length; i++) {       code = state.transforms[i](el, code)     }          return code   } }
  | 
 
在这里还可以发现v-for的优先级高于v-if
render 函数
在Vue项目的main.js文件中,存在以下代码:
调用render函数会得到传入的模板(.vue文件)对应的虚拟DOM,那么这个render函数是从哪里来的?它是如何将.vue文件转换成浏览器可识别的代码的呢?
render函数的来源有两种方式:
- 第一种就是经过模板编译生成render函数
 
- 第二种是我们自己定义在组件里的render函数,这种会跳过模板编译的过程
 
自定义的render

上面三种情况最后编译出的内容完全一样。
那么,你一定会有一个疑问,既然模板能自己编译自动生成,为什么还要提出自定义render?
原因有二:
- 自己把VNode写了,就会直接跳过模板编译,不用再经历模板编译过程中解析模板动态属性、事件、指令等,所以性能上会有所提升。这一点在下面的渲染优先级上有所体现。
 
- 还有一些情况,能让我们的代码写法更加灵活,更加方便简洁,不会冗余
 
(在Element-UI的组件源码中就有大量的重写render函数)
1.渲染优先级
Vue的生命周期中关于模板编译的部分,如下

从图中可以知道,如果有template,就不会管el,所以template比el的优先级更高。
那我们自己手写的render函数呢?
1 2 3 4 5 6 7 8 9 10 11 12 13
   | <div id='app'>     <p>{{ name }}</p> </div> <script>     new Vue({         el:'#app',         data:{ name:'沐华' },         template:'<div>掘金</div>',         render(h){             return h('div', {}, '好好学习,天天向上')         }     }) </script>
   | 
 
结果在页面上渲染出来的只有<div>好好学习,天天向上</div>。因此,render函数的优先级更高。
所以,综上所述:优先级:render函数 > template > outerHTML 
因为不管是el挂载的outerHTML,还是template,最后都会被编译成render函数,而如果已经有了render函数,就跳过前面的编译。这一点在源码中也有体现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | Vue.prototype.$mount = function ( el, hydrating ) {    el = el && query(el);    var options = this.$options;        if (!options.render) {      var template = options.template;            if (template) {        if (typeof template === 'string') {          if (template.charAt(0) === '#') {            template = idToTemplate(template);          }        } else if (template.nodeType) {          template = template.innerHTML;        } else {          return this        }            } else if (el) {        template = getOuterHTML(el);      }    }    return mount.call(this, el, hydrating)  };
  | 
 
2.更灵活的写法
以如下代码为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | <template>     <h1 v-if="level === 1">       <a href="xxx">         <slot></slot>       </a>     </h1>     <h2 v-else-if="level === 2">       <a href="xxx">         <slot></slot>       </a>     </h2>     <h3 v-else-if="level === 3">       <a href="xxx">         <slot></slot>       </a>     </h3> </template> <script>   export default {     props:['level']   } </script>
   | 
 
我们可以换一种方式,写出和上面编译效果一样的代码
1 2 3 4 5 6 7 8
   | <script>   export default {     props:['level'],     render(h){       return h('h' + this.level, this.$slots.default())     }   } </script>
   | 
 
或者下面这样,多次调用的时候就很方便
1 2 3 4 5 6 7 8 9
   | <script>   export default {     props:['level'],     render(h){       const tag = 'h' + this.level       return (<tag>{this.$slots.default()}</tag>)     }   } </script>
   | 
 
小结

