十、模板编译

Vue

# 模板编译介绍

模板编译的主要目的是将模板 (template) 转换为渲染函数 (render)

<div> 
    <h1 @click="handler">title</h1>
    <p>some content</p>
</div>
1
2
3
4

渲染函数 render

render (h) 
    { return h('div', [
        h('h1', { on: { click: this.handler} }, 'title'),
        h('p', 'some content') 
    ])
}
1
2
3
4
5
6

对比两种写法,写模板比写render函数的代码更简单,更直观,开发速度更快。

# 模板编译的作用

  • Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
  • 使用Vue 2.x的时候,用户只需要编写类似 HTML 的代码,其本质是Vue模板,通过编译器 将模板转换为返回 VNode 的 render 函数
  • .vue 文件其内部的模板在 webpack 在构建的过程中转换成 render 函数,webpack本身不支持编译模板,其内部是用vue-loader操作的。

# 模板编译的分类

  • 根据运行时间,我们可以把编译过程分成运行时编译和构建时编译(打包的时候编译),运行时编译的前提是必须使用完整版的vue,因为完整版的vue才带编译器,它在项目运行的时候才把模板编译成render函数,这种情况的缺点是体积大,运行速度慢。
  • 构建时编译,项目vue-cli用的就是不带编译器的版本,需要在打包的时候使用webpack 的vue-loader将模板进行编译,这种好处的不需要加载编译器的代码,体积小,运行速度快。

# 体验模板编译的结果

# 准备html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>compile</title>
</head>
<body>
  <div id="app">
    <h1>Vue<span>模板编译过程</span></h1>
    <p>{{ msg }}</p>
    <!-- 自定义组件,并添加了自定义事件,组件中没有处理这个事件 -->
    <comp @myclick="handler"></comp>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    // 全局组件comp
    Vue.component('comp', {
      template: '<div>I am a comp</div>'
    })
    // 创建vue实例
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello compiler'
      },
      methods: {
        handler () {
          console.log('test')
        }
      }
    })
    // 编译生成的render函数
    console.log(vm.$options.render)
  </script>
</body>
</html>
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

从入口文件中分析此时的模板是什么?

entry-runtime-with-compile.js

模板要么是render选项,要么是template选项,要么是el选项

从控制台看打印出来的模板是什么?格式化之后:

// 匿名函数
function anonymous() {
    //使用with,with的作用是在这个代码块中使用this对象成员的时候可以省略this
    // 下面的 _m。_v。和 _c 都是省略this的,这些是vue实例的方法
    with (this) {
        return _c(
            "div", 
            { attrs: { id: "app" } }, 
            [ 
                _m(0),
                _v(" "),
                _c("p", [ _v( _s( msg ) ) ]),
                _v(" "),
                _c("comp", { on: { myclick: handler }})
            ], 
            1
        );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • _c 是 createElement() 方法,定义的位置 instance/render.js 中,h函数,用于创建vnode对象
  • _m_v。和 _s相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中,这些函数在installRenderHelpers函数中,在core/instance/render.js中的renderMixin中调用,

# target._m = renderStatic

用来处理静态内容

# target._v = createTextVNode

创建文本的虚拟节点

export function createTextVNode (val: string | number) {
  // 返回一个VNode,里面只创建了一个text属性,代表文本节点
  return new VNode(undefined, undefined, undefined, String(val))
}
1
2
3
4

# target._s = toString

转换成字符串的方法

export function toString (val: any): string {
  // 判断参数是否为null,是就返回空字符串
  // 判断参数是否是数组或者对象,是就调用JSON.stringify转换成字符串
  // 否则就直接转换成字符串
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}
1
2
3
4
5
6
7
8
9
10

# 整体流程分析

function anonymous() {
    with (this) {
    // 核心方法是_c,createElement,h函数,创建并返回一个虚拟DOM,接收四个参数
        return _c(
            // 第一个参数:tag,标签名称
            "div", 
            // 第二个参数:dom属性
            { attrs: { id: "app" } },
            // 第三个参数:子节点,重点
            [ 
                // 处理静态内容,对应模板中的<h1>Vue<span>模板编译过程</span></h1>
                // 静态内容会做优化处理
                _m(0),
                // 用来创建空白的文本节点,对应h1和p标签之间的空白处
                // 如果两个标签中间有换行,也会生成空白节点
                _v(" "),
                // 创建p标签的vnode
                // 因为里面只有文本,那么在createElement的第二个参数中传文本内容,在createElement内部嗲用normalizeChildren将文本内容转换成数组包裹的vnode节点
                // 编译生成的render函数,这里就是数组形式,数组存储vnode节点,比我们手写的render函数少一步处理过程
                // 在之后手写render函数的时候,这里也可以直接写数组形式,其性能更好
                // _s 是toString函数,将用户输入的数据转换成字符串,因为用户输入的数据可能是任意类型
                _c("p", [ _v( _s( msg ) ) ]),
                // 空白文本节点
                _v(" "),
                // 通过_c创建组件VNode,之后解释
                _c("comp", { on: { myclick: handler }})
            ], 
            // 第四个参数,如何处理子节点,将来将children降维成一维数组
            1
        );
    }
}
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

# Vue Template Explorer

一个工具,网页工具,把html模板转换成render函数

Vue Template Explorer (opens new window)

image

Vue 3 Template Explorer (opens new window)

image

可以看到vue2和vue3的内容不一样,Vue3这里做了优化.下面大致说一下里面的内容

import { createVNode as _createVNode, createTextVNode as _createTextVNode, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode, resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_comp = _resolveComponent("comp")

  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, [
      _createTextVNode("Vue"),
      _createVNode("span", null, "模板编译过程")
    ]),
    // p标签中通过上下文获取msg的值
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    _createCommentVNode(" 自定义组件,并添加了自定义事件,组件中没有处理这个事件 "),
    _createVNode(_component_comp, { onMyclick: _ctx.handler }, null, 8 /* PROPS */, ["onMyclick"])
  ]))
}

// Check the console for the AST
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 找模板编译的入口函数

  1. 从web/entry-runtime-with-compiler.js中找到compileToFunctions函数,这个函数的作用就是将template转换成render函数,从这个函数下手:
// 把 template 转换成 redner 函数
  // staticRenderFns是一个数组
const { render, staticRenderFns } = compileToFunctions(template, {
    outputSourceRange: process.env.NODE_ENV !== 'production',
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
}, this)
// 将render和staticRenderFns记录到对应的options属性中
options.render = render
options.staticRenderFns = staticRenderFns
1
2
3
4
5
6
7
8
9
10
11
12
  1. 下面跳转到compileToFunctions函数中,ctrl+左键,这个时候跳转到了web/compile/index.js,可以看到,compileToFunctions是由函数createCompiler返回的,调用这个函数的时候传入了baseOptions选项
const { compile, compileToFunctions } = createCompiler(baseOptions)
1
  1. 下面看一下baseOptions里面的内容:这是跟web平台相关的选项,在web/compiler/options.js
export const baseOptions: CompilerOptions = {
  //html相关
  expectHTML: true,
  // 模块--> modules/index.js
  // 当前的模块是用来处理类样式和行内样式的,以及处理和v-if一起使用的v-model,在modules/model.js的注释中可以一看到
  modules,
  // 指令-->directives/index.js
  // 处理v-model,v-text,v-html的指令,处理的是模板中的指令
  directives,
  // 是否是pre标签
  isPreTag,
  // 是否是自闭合标签
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  // 是否是html中的保留标签
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 还是回到createCompiler,看下引用的路径,,在src/compiler/index.js下面,与平台无关的代码

createCompiler是通过createCompilerCreator这个函数返回,createCompilerCreator这个函数接收一个函数参数baseCompile

这个是核心函数,接收参数一个是template,一个是用户传入的options,

最后将抽象语法树,render和staticRenderFns返回,这个返回的就是我们前面步骤1中返回的东西

export const createCompiler = createCompilerCreator(function baseCompile (
  // baseCompile接收模板和合并后的选项参数
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 里面做了三件事情
  // 1.把模板转换成 ast 抽象语法树
  // 抽象语法树,用树形的方式描述代码结构
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 2.优化抽象语法树
    optimize(ast, options)
  }
  // 3.把抽象语法树生成字符串形式的js代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 下面看一下createCompilerCreator函数内部做了什么,这个文件在src/compile/create-compiler.js中
    首先看到了,定义函数的时候参数是baseCompile,在createCompilerCreator里面,定义了compile函数,里面先根据平台的参数和用户传入的参数进行合并得到的最终参数finalOptions,之后使用了baseCompile并传入了finalOptions,之后函数将compile和compileToFunctions返回
export function createCompilerCreator (baseCompile: Function): Function {
  // baseCompile 平台相关options
  // 这个函数返回了一个createCompiler函数
  return function createCompiler (baseOptions: CompilerOptions) {
    // createCompiler在中定义了一个compile函数,用来接收模板和用户传递的选项两个参数
    // 在这个函数中会把与平台相关的选项和用于传入的选项参数进行合并
    // 再调用baseCompile把合并后的选项传递给它
    // 这是通过函数,返回函数的一个目的
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 合并参数的过程,省略
      ...
      //调用baseCompile,传入template和合并之后的选项,
      // 里面将template转化成ast语法树,优化语法树之后将语法树转化成js代码
      // 返回的值是
      /**
       * compiled : 
       * return {
          ast,
          render: code.render,
          staticRenderFns: code.staticRenderFns
        }
       */
      const compiled = baseCompile(template.trim(), finalOptions)
      ...
      return compiled
    }

    // 最后返回了compile和compileToFunctions
    // compileToFunctions是createCompileToFunctionFn返回的,这个函数是模板编译的入口
    /**
     * compiled : 
     * return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
      */
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
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
  1. compileToFunctions是啥,可以看到在return中,他是由createCompileToFunctionFn(compile)调用得到的,说明createCompileToFunctionFn(compile)函数返回的就是compileToFunctions,下面跳转到createCompileToFunctionFn中看起是怎么定义的

  2. 这个函数在src/compile/to-function.js中,可以看到函数定义的时候需要参数compile函数,然后返回了一个函数compileToFunctions

export function createCompileToFunctionFn (compile: Function): Function {
...
    return function compileToFunctions (
        template: string,
        options?: CompilerOptions,
        vm?: Component
      ): CompiledFunctionResult {
      ...
    }
}    
1
2
3
4
5
6
7
8
9
10

这么绕的目的,就是为了封装到,只接收template和一些参数,然后返回的就是render函数和staticRenderFns数组。

下面看一个整理的图:

image

所以createCompileToFunctionFn是我们的模板编译的入口函数。

# 模板编译过程

# createCompileToFunctionFn

  1. 找缓存中编译的结果
  2. 开始编译
  3. 将字符串的js代码转换成js方法
  4. 缓存并返回
export function createCompileToFunctionFn (compile: Function): Function {
  // 创建了没有原型的对象,目的为了通过闭包缓存编译之后的结果
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 将options克隆了一份,是vue实例化传入的options
    options = extend({}, options)
    // 开发环境中在控制台发送警告
    const warn = options.warn || baseWarn
    delete options.warn

    ...

    // check cache
    // 1. 是否有编译的结果,如果有直接把编译的结果返回,不需要重新编译
    // 这里是用空间换时间,这里的key是把模板作为key
    // options的这个属性只有完整版的才有,只有编译的时候才会使用到
    // 其作用是改变插值表达式使用的符号,插值表达式默认的是使用{{}}
    // 通过这个属性可以把插值表达式所使用的符号改成任意的内容
    // 例如es6的模板字符串,官方文档中有相应的解释
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 2. 开始进行编译,把模板和用户传入的选项作为参数
    // 编译结束compiled:{ render, staticRenderFns },此时的render中存储的是js的字符串形式
    // 这个对象中还有两个辅助的属性,compiled.errors和compiled.tips
    // 在编译模板的过程中,会收集模板中遇到的错误和一些信息
    const compiled = compile(template, options)

    // check compilation errors/tips
    // 在开发环境中,把compiled.errors和compiled.tips遇到的错误和一些信息打印出来
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        ...
      }
      if (compiled.tips && compiled.tips.length) {
        ...
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []

    // 3. 把字符串形式的js代码转换成js方法
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // 检查把错误信息打印出来
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        ...
      }
    }

    //4. 缓存并返回res对象(render, staticRenderFns)
    return (cache[key] = res)
  }
}

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# compile

最终编译是调用了compile函数,这个函数是在createCompiler中定义的.

  1. 合并baseOptions和compile参数options
  2. 调用baseCompile,传入template和合并之后的选项,得到编译后的js字符串形式的render
  3. 将错误和信息保存返回
export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    // createCompiler在中定义了一个compile函数,用来接收模板和用户传递的选项两个参数
    // 在这个函数中会把与平台相关的选项和用于传入的选项参数进行合并
    // 再调用baseCompile把合并后的选项传递给它
    // 这是通过函数,返回函数的一个目的
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 原型指向了baseOptions,作用是合并baseOptions和compile参数options
      const finalOptions = Object.create(baseOptions)
      // 存储编译过程中存储的错误和信息
      const errors = []
      const tips = []
      //把消息放到对应的数组中
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      // 如果options存在的话,开始合并baseOptions和optinos
      if (options) {
        ...
      }
      ...
      
      //调用baseCompile,传入template和合并之后的选项,
      // 里面将template转化成ast语法树,优化语法树之后将语法树转化成js代码
      // 返回的值是
      /**
       * compiled : 
       * return {
          ast,
          render: code.render,(js字符串形式的代码)
          staticRenderFns: code.staticRenderFns
        }
       */
      const compiled = baseCompile(template.trim(), finalOptions)
      ...
      // 会将errors和tips数组的信息赋值给compiled的属性errors和tips
      compiled.errors = errors
      compiled.tips = tips
      // 将编译好的对象返回
      return compiled
    }
    ...
  }
}
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
48

# baseCompile

模板编译的核心函数,里面做了三件事情:

1 把模板转换成 ast 抽象语法树
2 优化抽象语法树
3 把抽象语法树生成字符串形式的js代码

最后将render和staticRenderFns返回.此时的render函数并不是最后的render函数,而是字符串形式的render函数.


1

什么是抽象语法树?

抽象语法树简称AST,使用对象的形式描述树形的代码结构,对象中记录父子节点形成树的结构,此处的抽象语法树是用来描述树形结构的HTML字符串

会先把HTML字符串解析成AST,然后记录HTML标签的必要属性以及解析Vue中的一些指令,把解析后的指令记录到AST

为什么要使用抽象语法树?

把模板字符串转换成AST后,可以通过AST对模板做优化处理.标记模板中的静态内容,也就是纯文本内容.在patch的时候直接跳过静态内容,即静态子树,不需要对其进行对比和重新渲染,优化性能.

使用babel对代码进行降级处理的时候,也是会先把代码转换成AST,再把AST转换成降级之后的js代码

怎么查看AST

AST explorer (opens new window)

可以选择不同的语言,生成的AST

image

旁边还可以选择Vue内部提供的解析器,下面是Vue3的解析器

image

下面是vue2.6的解析器

image

下面左边是模板字符串,后面是编译好的AST,对象的形式

image

  • type属性,用来记录节点的类型
    • 1:标签
    • 3:文本
  • tag标签
  • attrsList,attrsMap,rawAttrsMap记录标签属性
  • children子节点
  • static:为true表示静态节点

生成AST的整个过程

# parse

作用:模板字符串转换成AST对象,这个过程比较复杂,Vue内部借鉴了一个库区解析HTML

因为parse太过复杂,这里只关注整体的执行流程,并不对parse进行深入分析.

  • 参数:parse函数接收两个参数,一个是模板字符串,去除了前后空格,和合并后的选项
  • 返回:ast对象
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 1. 解析options
  warn = options.warn || baseWarn
  ...

  // 定义了一些变量和函数
  ...

  // 2.对模板解析
  // 接收模板字符串,第二个参数是一个对象,将选项的成员传入
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 解析过程中的回调函数,生成AST
    // 分别开始标签,结束标签,文本内容,注释标签的时候去执行
    start (tag, attrs, unary, start, end) {
      ...
    },

    end (tag, start, end) {
      ...
    },

    chars (text: string, start: number, end: number) {
      ...
    },

    comment (text: string, start, end) {
      ...
    }
  })
  // 返回root变量,里面存储了解析好的ast对象
  return root
}
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

所以下面对parseHTML进行分析

# parseHTML

依次去遍历模板字符串,把HTML模板字符串转换成AST对象,也就是普通的对象,字符串中的指令都会记录在对象的相应属性上

// 接收模板字符串,第二个参数是一个对象,将选项的成员传入
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 解析过程中的回调函数,生成AST
    // 分别开始标签,结束标签,文本内容,注释标签的时候去执行
    start (tag, attrs, unary, start, end) {
      // check namespace.
      // 创建AST对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }
      // 之后给AST对象进行赋值
      ...

      // 开始处理指令
      if (!inVPre) {
      // 在下面的函数中有定义
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      ...
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 处理结构化指令
        // v-for v-if v-once
        processFor(element)
        processIf(element)
        processOnce(element)
      }
      ...
    },

    end (tag, start, end) {
      ...
    },

    chars (text: string, start: number, end: number) {
      ...
    },

    comment (text: string, start, end) {
      ...
    }
  })
  
function processPre (el) {
  // 获取v-pre指令,如果有值就在ast中移除对应的属性
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true
  }
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

这里面的getAndRemoveAttr,就是获取了其对应的属性,然后从attrsMap中删除,并返回属性的值

export function getAndRemoveAttr (
  el: ASTElement,
  name: string,
  removeFromMap?: boolean
): ?string {
  let val
  // 先获取name对应的属性
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList
    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        list.splice(i, 1)
        break
      }
    }
  }
  // 最后在从el.attrsMap中移除,最后返回这个属性的值
  if (removeFromMap) {
    delete el.attrsMap[name]
  }
  return val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

进入之后看下html-parser.js,开始注释说借鉴了开源库simplehtmlparser,上面还定义了很多正则表达式,这些正则表达式的作用是用来匹配html模板字符串中的内容

// 匹配标签中的属性,包括指令
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 开始标签<
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 开始标签>
const startTagClose = /^\s*(\/?)>/
// 结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 文档声明
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
// 文档中的注释
const comment = /^<!\--/
const conditionalComment = /^<!\[/
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

下面定义的parseHTML函数,遍历HTML,html就是我们的模板字符串,会把处理完的文本截取掉,继续去处理剩余的部分

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  // 遍历HTML,html就是我们的模板字符串,会把处理完的文本截取掉,继续去处理剩余的部分
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              // 如果当前找到注释标签,并且找到comment方法后,这个是调用parseHTML方法传递进来的方法
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            //记录当前的位置,在处理完毕的位置截取剩余的内容
            advance(commentEnd + 3)
            // 后面继续去处理剩余的HTML,知道处理完毕
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // 匹配是否是条件注释
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype:
        // 匹配是否是文档声明
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag:
        // 是否是结束标签
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        // 是否是开始标签
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
        }
      }

      let text, rest, next
      ...
    } else {
      ...
    }
    ...
  }
  ...

  function parseStartTag () {
    ...
  }

  function handleStartTag (match) {
    // 这里处理了很多内容,还处理了属性
    const tagName = match.tagName
    const unarySlash = match.unarySlash

    if (expectHTML) {
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }
    ...
    // 在开始标签处理完毕之后,最终调用start方法,并把解析好的标签名,属性,是否是一元标签,起始位置都传递过去
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  function parseEndTag (tagName, start, end) {
    ...
  }
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

# optimize优化

if (options.optimize !== false) {
    // 2.优化抽象语法树
    optimize(ast, options)
}
1
2
3
4

进入optimize函数可以看到有注释,意思是优化的目的是为了标记抽象语法树中的静态节点,静态节点对应的DOM子树永远不会发生变化,比如一个纯文本的div,就不会发生变化.以后就不会重新渲染,之后编译的时候就会跳过静态子树.

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  // 判断root,是否传递了ast对象,如果没有直接返回
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 标记静态节点
  markStatic(root)
  // second pass: mark static roots.
  // 标记静态根节点
  markStaticRoots(root, false)
}
1
2
3
4
5
6
7
8
9
10
11
12

下面进入markStatic,寿险判断当前astNode是否是静态节点

function isStatic (node: ASTNode): boolean {
  // 首先判断node中的type属性,如果是2的话说明是表达式,不是静态节点
  if (node.type === 2) { // expression
    return false
  }
  // 如果是3说明是文本节点,是静态节点,返回true
  if (node.type === 3) { // text
    return true
  }
  // 如果下面的条件都满足,说明是静态节点,返回true
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in 不能是内置组件
    isPlatformReservedTag(node.tag) && // not a component 不能是组件
    !isDirectChildOfTemplateFor(node) && // 不能是v-for下的直接子节点
    Object.keys(node).every(isStaticKey)
  ))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

然后继续判断如果type是1,说明是标签,那么需要判断其子节点

function markStatic (node: ASTNode) {
  // 判断当前 astNode 是否是静态的
  node.static = isStatic(node)
  // 元素节点
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    // 判断是不是保留标签,如果不是保留标签,那就是组件
    // 如果是组件,不会把组件中的slot标记成静态节点,如果组件中的slot被标记为静态的
    // 那他将来就没有办法改变
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    // 遍历ast对象的所有子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      // 递归调用markStatic标记静态
      markStatic(child)
      if (!child.static) {
        // 如果有一个 child 不是 static,当前 node 不是 static
        node.static = false
      }
    }
    // 处理条件渲染中的AST对象,与上一步处理相同
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

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

标记子节点完成,下面看一下标记静态根节点markStaticRoots

静态根节点指的是:标签包含字标签,并且没有动态内容,都是纯文本内容,如果内容中只有文本内容没有字标签,vue不会对其做优化处理.

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  // 判断当前根节点是否是元素节点
  if (node.type === 1) {
    // 判断该节点是否是静态的或者只渲染一次,来标记该节点在循环中是否是静态的
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    
    // 标记静态根节点,首先是静态的并且有子节点
    // 并且这个节点中不能只有文本类型的子节点,
    // 如果一个元素内只有文本节点,这个元素不是静态的Root
    // Vue认为这种这种优化成本大于收益
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    // 检测当前节点的子节点中是否有静态的Root,递归调用markStaticRoots
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    // 遍历条件渲染的子节点,递归调用markStaticRoots
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}
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

# generate

把抽象语法树生成字符串形式的js代码,接收两个参数:一个是ast对象,一个是options参数,返回的code就是js代码

const code = generate(ast, options)
1

进入generate函数看看,里面结构比较清晰

// 重点关注静态根节点的处理过程
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 创建代码生成过程中使用的状态对象CodegenState
  const state = new CodegenState(options)
  // 判断如果ast存在,调用genElement开始生成代码,否则直接返回_c("div")
  // 核心核心
  const code = ast ? genElement(ast, state) : '_c("div")'
  // 返回render和staticRenderFns
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

进入CodegenState构造函数

constructor (options: CompilerOptions) {
    // 存储了和代码生成相关的属性和方法
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    // 下面是重点关注的两个属性
    this.staticRenderFns = [] // 存储静态根节点生成的代码,因为一个模板中可能有多个根节点,数组里面存储的是字符串形式的代码
    this.pre = false // 当前处理的节点,是否是用v-pre标记的
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# genElement

下面进入核心代码genElement函数,这个函数是最终把ast对象转换成代码的位置

export function genElement (el: ASTElement, state: CodegenState): string {
  // 判断当前的ast对象是否有parent属性
  // 当前的pre去记录pre或者父节点的pre
  // 如果是父节点有v-pre的话,那么子节点也是静态的
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  // 如果当前已经处理过静态根节点就不再处理
  // staticProcessed属性是用来标记当前属性是否被处理了
  // genElement会被递归调用,这里判断的目的就是防止重复处理
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  // 下面出依次处理once,for,if指令,把他们转换成相应的代码
  } 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)
  // 如果是template标签,判断其不是slot或者pre,说明不是静态的,接下来会生成内部的子节点,以及对应的代码
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
  // 如果没有子节点,返回void 0,也就是undefined
    return genChildren(el, state) || 'void 0'
  // 处理slot标签
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  // 如果上面都不满足,下面处理组件以及内置的标签
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      // 这里只考虑普通标签的处理情况
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 生成元素的属性/指令/事件等
        // 处理各种指令,包括 genDirectives(model/text/html)
        // 这里会把ast对象的相应属性转换成createElement所需要的data对象的字符串形式
        data = genData(el, state)
      }

      // 处理子节点,把el中的子节点转换成createElement中需要的数组形式,也就是第三个参数
      // 调用genChildren的时候传了三个参数,调用完之后就生成了render函数中需要的js代码
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 调用_c,传入标签,data和children
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    // 返回生成的代码
    return code
  }
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# genData
  • genData里面拼的是普通的js对象的字符串形式,会根据el对象的属性去拼接相应的data,最后返回data,返回的data就是createElement的第二个参数
# genChildren
  • genChildren的作用就是把每一个ast对象,通过调用genNode生成对应的代码形式,最后把数组中的每一项通过join合并成逗号分隔的字符串,最后就再拼接上createElement的最后一个参数,即如何让数组降维,
export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  // 先判断ast对象是否有子节点
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    // 首先获取如何处理数组,即createElement的第四个参数
    // 数组是否需要被降维
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    // 获取了gen函数,这个函数首先会获取altGenNode,这个是genChildren的第四个参数,刚才调用的时候没有传这个参数,所以此时这个没有值,
    // 返回的是genNode
    const gen = altGenNode || genNode
    // 调用map遍历数组中的每一个元素,使用刚获取到的gen函数对每一个元素处理并且返回
    // map最终将所有的子节点通过gen函数转换成了代码,然后通过join把数组中的元素,把逗号进行分割,返回了字符串,把结果存储到数组中
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
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

下面看gen函数,gen函数其实就是genNode,跳转看genNode

function genNode (node: ASTNode, state: CodegenState): string {
  //判断当前ast对象的类型,如果是标签,继续调用genElement处理当前的子节点
  if (node.type === 1) {
    return genElement(node, state)
  // 如果type是3,并且是注释节点,调用genComment生成注释节点的代码
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
    // 处理文本节点,里面返回了render函数中的代码
  } else {
    return genText(node)
  }
}

export function genText (text: ASTText | ASTExpression): string {
  // 用来创建文本的vnode节点
  // 如果type是2,此时处理的是表达式,直接返回该表达式,表达式已经使用了toString函数转换成了字符串
  // 下面还使用了JSON.stringify转成字符串,还用了transformSpecialNewlines函数,这个函数的作用是将代码中一些特殊的换行,unicode形式的进行修正,防止意外情况
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

export function genComment (comment: ASTText): string {
  // 创建了一个被标识为comment的vnode节点
  // 参数JSON.stringify,给字符串加引号 hello -> "hello"
  // 因为最后生成的是字符串形式的代码
  return `_e(${JSON.stringify(comment.text)})`
}
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
# 静态根节点

那静态根节点是怎么处理的呢?

在generator返回值render和staticRenderFns中,render是生成的ast对象对应的字符串形式,staticRenderFns是数组,那这个数组中是什么时候去添加元素的?添加的元素又是什么呢?

通过名字可以知道,这个里面放的应该是生成的静态渲染函数,即根节点对应的渲染函数.

给staticRenderFns添加函数是在genStatic内部,genElement内部如果el是根节点并且没有被处理过的话,就会调用genStatic函数,

function genStatic (el: ASTElement, state: CodegenState): string {
  // 首先标记staticProcessed属性为true,即当前节点已经被处理过了
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  // 将state.pre暂存到一个变量中
  const originalPreState = state.pre
  // 获取ast中的pre属性赋值给state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  //把静态根节点转换成生成vnode的对应js代码,这里调用了genElement,这个时候staticProcessed已经标记为处理过,所以不用再处理
  // 这里使用数组是因为,一个模板中可能有多个静态子节点,这个是先把每一个静态子树对应的代码进行存储,最后返回的是当前节点对应的代码,下面返回了_m的调用,传入了当前节点在renderFunctions数组中对应的索引,即把刚刚生成的代码传递进来
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  // 处理完成之后将state.pre还原
  state.pre = originalPreState
  // 注意,这里最终传递的是函数的形式,因为这些字符串形式的代码,都会被转化成函数
  // _m : renderStatic
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}
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

之后会到genElement中,因为已经标记为处理过,所以不会再重复处理,静态根节点不满足下面判断的需求,所以直接跳过到else中生成对应的代码.这里不再赘述.

_m指的就是renderStatic函数,在core/instance/render-helpers/index.js中

target._m = renderStatic
1

下面看renderStatic函数内部是如何实现的

export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  // 首先从缓存中获取静态根节点对应的代码
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  // 如果没有的话就去staticRenderFns数组中获取静态根节点对应的render函数,然后调用,此时就生成了vnode节点,然后把结果缓存
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  // 调用markStatic,作用是把当前返回的vnode节点标记为静态的
  markStatic(tree, `__static__${index}`, false)
  return tree
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这里里面用到了markStatic函数,重点看一下

function markStatic (
  tree: VNode | Array<VNode>,
  key: string,
  isOnce: boolean
) {
  // 作用是把当前返回的vnode节点标记为静态的
  // 如果当前的tree是数组的话,会遍历数组中所有的vnode,然后调用markStaticNode,否则直接调用markStaticNode标记为静态的
  // vnode被标记为静态之后,将来调用patch函数的时候,他内部会判断,如果当前vnode是静态的,不再对比节点的差异,直接返回.因为静态节点不再发生变化,不需要进行处理,这是对静态节点的优化,如果静态节点已经渲染到了文档上,那此时它不需要重新被渲染
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== 'string') {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    markStaticNode(tree, key, isOnce)
  }
}

function markStaticNode (node, key, isOnce) {
  // 这个把vnode节点设置为静态的
  node.isStatic = true
  // 记录key和isOnce
  node.key = key
  node.isOnce = isOnce
}
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

整个静态根节点的处理过程就完成了,比较复杂,但是还没有完.因为我们只看到了把静态根节点转换成字符串形式的代码,最后再回顾一下把字符串转换成函数的过程,

下面会到generate被调用的位置,在baseCompile中返回的render和staticRenderFns都是代码的字符串形式

所以当这个函数执行完毕之后要转换成函数,这个函数在compile中被调用,而最后在入口函数中,createFunction把字符串转化成了js方法,最后缓存并且返回

# 调试模板编译

# 模板编译过程总结

image

更新时间: 2022-01-26 22:04