核心编译

compile函数里所使用的baseCompile函数是在调用createCompilerCreator函数时传入的。baseCompile函数里的逻辑是核心的编译流程,与平台无关,具体包括:

  • 解析模板字符串,创建 AST
  • 标记 AST Tree 里可优化的节点
  • 基于 AST 生成字符串形式的render/staticRenderFns

最后返回对象{ ast, render, staticRenderFns }

// src/compiler/index.js

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 解析模板字符串,创建 AST
  const ast = parse(template.trim(), options)

  // 标记 AST Tree 里可优化的节点
  if (options.optimize !== false) {
    optimize(ast, options)
  }

  // 基于 AST 生成字符串形式的`render`/`staticRenderFns`
  const code = generate(ast, options)
  return {
    ast,
    // 字符串形式的 render/staticRenderFns
    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
22
23
24
25
26
27
28
29
30
31

解析模板字符串,创建 AST

parse函数接收templateoptions为参数,返回 AST 的根节点(及 AST Tree),详情请见解析模板字符串,创建 AST

标记 AST Tree 里可优化的节点

获取到 AST Tree 之后,需要识别并标记其中的静态子树(比如永远不需要改变的 DOM),一旦我们检测到这些静态子树:

  • 可以将他们提升为常量,在以后的每一次调用render方法时不需要再为它们创建新的 VNode
  • patch阶段可以完全跳过它们

详情请见优化 AST 树

生成 render 函数

根据 AST Tree 生成renderstaticRenderFns的字符串形式,详情请见生成 render 函数

options 对象

因为在baseCompile函数里的三大步骤都需要使用到options对象,我们需要确定下 Web 平台下的options对象是如何生成的。

// src/platforms/web/entry-runtime-with-compiler.js
// ...
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ...
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    // ...
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 将 template 编译成 render 函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        // 编译模板时,是否要对换行符(\n)进行解码
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 改变纯文本插入分隔符。new Vue({ delimiters: ['${', '}'] }) // 分隔符变成了 ES6 模板字符串的风格
        // 详见 https://cn.vuejs.org/v2/api/#delimiters
        delimiters: options.delimiters,
        // 若 comments 为 true,将会保留渲染模板中的 HTML 注释。默认行为是舍弃它们。
        // 详见 https://cn.vuejs.org/v2/api/#comments
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      // ...
    }
  }
  return mount.call(this, el, hydrating)
}
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

Web 平台上的Vue.prototype.$mount方法里,若组件实例上不存在vm.$options.render方法,就会调用compileToFunctions函数生成render方法,compileToFunctions函数的第二个参数就是外部传入的options,开发者在声明组件的选项对象时可以传入部分选项,用于之后render函数的生成,其中的每个字段都已经在上面代码里说明。

// src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  // 最终的 compileToFunctions 函数,返回 { render, staticRenderFns }
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    // ...

    // check cache
    // 优先使用缓存结果
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 编译
    const compiled = compile(template, options)
    // ...
  }
}
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

compileToFunctions函数里,基本上原封不动地将外部传入的options传入到了compile函数里。在compile函数里,会基于 Web 平台相关的基础选项对象baseOptions合并外部传入的选项对象options,形成最终传入baseCompile函数里的选项对象finalOptions

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      // options 是外部传入的选项对象,方便开发者可以控制 render 函数的生成。
      // 开发者可以在组件的选项对象里声明相关选项,这些选项对象将在 src/platforms/web/entry-runtime-with-compiler.js 里调用 compileToFunctions 函数时传入,经过在 compileToFunctions 函数里调用 compile 函数传入到这里
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      // 合并 baseOptions 和传入的平台相关的 options
      // modules 是数组,合并数组
      // directives 是对象,合并对象,options.directives 优先使用
      // 其他属性优先使用 options.xxx
      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length

          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    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
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

baseOptions是在调用createCompiler里传入的,包含 Web 平台相关的一些判断函数和模块。

// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
1
2
3
4
5
6
7
// src/platforms/web/compiler/options.js
import {
  isPreTag,
  mustUseProp,
  isReservedTag,
  getTagNamespace
} from '../util/index'

import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  // 函数,判断是否是 <pre> 标签
  isPreTag,
  // 函数,判断是否是一元标签,即一定不会自我闭合,比如 <br>、<hr>
  isUnaryTag,
  // 函数,判断哪些标签的那些 attribute 需要用 props 来实现数据绑定
  mustUseProp,
  // 函数,判断哪些标签是无需显示闭合的
  canBeLeftOpenTag,
  // 保留标签(HTML 标签及 SVG 标签)
  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
21
22
23
24
25
26
27
28
29
30
31

经过在compile函数里的合并之后,最终传入baseCompile函数的finalOptions的结构为:

finalOptions = {
  outputSourceRange: process.env.NODE_ENV !== 'production',
  // 编译模板时,是否要对换行符(\n)进行解码
  shouldDecodeNewlines,        // src/platforms/web/util/compat.js
  shouldDecodeNewlinesForHref, // src/platforms/web/util/compat.js
  // 改变纯文本插入分隔符。new Vue({ delimiters: ['${', '}'] }) // 分隔符变成了 ES6 模板字符串的风格,详见 https://cn.vuejs.org/v2/api/#delimiters
  delimiters: options.delimiters, // options 是组件(合并后的)选项对象
  // 若 comments 为 true,将会保留渲染模板中的 HTML 注释。默认行为是舍弃它们。详见 https://cn.vuejs.org/v2/api/#comments
  comments: options.comments,     // options 是组件(合并后的)选项对象


  /** 这些属性/方法是从原型上复制过来的 - 开始 **/
  expectHTML: true,
  // 函数,判断是否是 <pre> 标签
  isPreTag, // src/platforms/web/util/element.js
  // 函数,判断是否是一元标签,即一定不会自我闭合,比如 <br>、<hr>
  isUnaryTag, // src/platforms/web/compiler/util.js
  // 函数,判断哪些标签的那些 attribute 需要用 props 来实现数据绑定
  mustUseProp, // src/platforms/web/util/attrs.js
  // 函数,判断哪些标签是无需显示闭合的
  canBeLeftOpenTag, // src/platforms/web/compiler/util.js
  // 保留标签(HTML 标签及 SVG 标签)
  isReservedTag, // src/platforms/web/util/element.js
  // 函数,获取标签的命名空间
  getTagNamespace, // src/platforms/web/util/element.js
  staticKeys: 'staticClass,staticStyle'
  /** 这些属性/方法是从原型上复制过来的 - 结束 **/


  __proto__: {
      modules: [
        klass, // src/platforms/web/compiler/modules/class.js
        style, // src/platforms/web/compiler/modules/style.js
        model, // src/platforms/web/compiler/modules/model.js
      ],
      directives: {
        model, // src/platforms/web/compiler/directives/model.js
        text,  // src/platforms/web/compiler/directives/text.js
        html,  // src/platforms/web/compiler/directives/html.js
      },
  }


  warn, // 警告函数
}
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