vue源码分析(八) 编译之整体流程

10/8/2018 vue源码分析面试

# 1. 概述

​ 模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpackvue-loader 事 先把模板编译成 render 函数。

# 2. 逻辑流程

​ 当我们使用 Runtime + CompilerVue.js,它对 $mount 函数的定义。

# 2.1 $mount

​ 源码目录:src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /*  */
  
  // resolve template/el and convert to render function
  if (!options.render) {
    /*  */
    if (template) {
      /*  */

      // 模板编译成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        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

​ 这段代码中 compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns

# 2.2 compileToFunctions

​ 接下来我们看一下 compileToFunctions 的定义。

​ 源码目录: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

​ 从上面的代码我们可以看出,compileToFunctions 方法实际上是执行的 createCompiler(baseOptions) 方法,该方法接收一个编译配置参数。

# 2.3 createCompiler

​ 我们继续来看 createCompiler 的定义。

​ 源码目录:src/compiler/index.js

// `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 {
  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
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

​ 从上面的代码我们可以看出,createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都是在这个 baseCompile 函数中执行的。

# 2.4 createCompilerCreator

​ 我们继续来看 createCompilerCreator 的定义。

​ 源码目录:src/compiler/create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
        / * * /
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 从上面的代码我们可以看出,createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值。

# 2.5 createCompileToFunctionFn

​ 我们继续来看 createCompileToFunctionFn 的定义。

​ 源码目录:src/compiler/to-function.js

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    / * * /
  }
}
1
2
3
4
5
6
7
8
9
10
11

​ 至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template ,编译配置 optionsVue 实例 vm

​ 其中,核心的编译过程就一行代码:

​ 源码目录:src/compiler/to-function.js

const compiled = compile(template, options)
1

compile 函数在执行 createCompileToFunctionFn 的时候作为参数传入,它是 createCompiler 函数中定义的 compile 函数。

​ 源码目录:src/compiler/create-compiler.js

compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:

const compiled = baseCompile(template.trim(), finalOptions)
1

baseCompile 在执行createCompilerCreator 方法时作为参数传入,如下:

​ 源码目录:src/compiler/index.js

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  / * * /
}
1
2
3
4
5
6

​ 所以编译的入口我们终于找到了。

说明:关于baseCompile,我们后面小节会详细说明。

​ 编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧 很好的实现了 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

# 3. 逻辑关系图

逻辑关系图

# 4. 源码分析

# 4.1 createCompileToFunctionFn

​ 源码目录:src/compiler/to-function.js

/**
 * 创建compileToFunctions函数
 * @param {compile函数} compile 
 */
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 使用 extend 函数将 options 的属性混合到新的对象中并重新赋值 options
    options = extend({}, options)
    // 检查选项参数中是否包含 warn,如果没有则使用 baseWarn
    const warn = options.warn || baseWarn
    // 将 options.warn 属性删除
    delete options.warn

    /* istanbul ignore if */
    // 检测 new Function() 是否可用
    // 1、放宽你的CSP策略(内容安全策略)
    // 2、预编译
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    // 如果 options.delimiters 存在,则使用 String 方法将其转换成字符串并与 template 拼接作为 key 的值,否则直接使用 template 字符串作为 key
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    // 判断 cache[key] 是否存在,如果存在直接返回 cache[key]
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 编译模板
    const compiled = compile(template, options)

    // check compilation errors/tips
    // 检查使用 compile 对模板进行编译的过程中是否存在错误和提示
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    // 错误收集数组
    const fnGenErrors = []
    // 创建render
    res.render = createFunction(compiled.render, fnGenErrors)
    // 创建 staticRender
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    // 如果在生成渲染函数过程中有错误,则报警告
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }

    // 返回结果并将结果缓存
    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
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
107
108
109
110
111
112
113

​ 这个函数主要作用我们在上面 2.5 小节已经分析过了,接下来我们主要分析返回函数的作用,在分析之前我们先来看一下参数的含义,如下:

​ (1)、template:模板字符串

​ (2)、options

outputSourceRange: 生产环境还是开发环境

shouldDecodeNewlines: 默认flaseIE 在属性值中编码换行,而其他浏览器则不会

shouldDecodeNewlinesForHref:默认 truechromea[href] 中编码内容

delimiters: options.delimiters:改变纯文本插入分隔符。修改指令的书写风格,比如默认是 delimiters: ['${', '}']之后变成这样 ${mgs}

comments: options.comments:当设为 true 时,将会保留且渲染模板中的 HTML 注释,默认行为是舍弃它们

​ (3)、vm:Vue 实例

​ 接下来我们分析,这个返回函数主要做了哪些事情,如下:

​ (1)、获取 warn 函数

​ (2)、检测 new Function() 是否可用

​ (3)、编译模板

​ (4)、检查使用 compile 对模板进行编译的过程中是否存在错误和提示

​ (5)、创建 render 函数和 staticRender 函数

​ (6)、如果在生成渲染函数过程中有错误,则报警告

​ (7)、返回结果并将结果缓存

# 4.2 compile

​ 接下来我们分析一下 compile ,源码如下:

​ 源码目录:src/compiler/create-compiler.js


/**
 * 创建createCompiler函数
 * @param {baseCompile函数} baseCompile 
 */
export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    /**
     * 板编模译
     * @param {模板字符串} template 
     * @param {选项,参考 src/platforms/web/entry-runtime-with-compiler.js } options 
     */
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 通过 Object.create 函数以 baseOptions 为原型创建 finalOptions
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      // 定义 warn 函数
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        // 开发环境覆盖warn函数
        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
        // 给finalOptions上添加其他属性
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      // 给finalOptions上添加warn方法
      finalOptions.warn = warn

      // 调用baseCompile,编译模板。baseCompile定义在 src/compiler/index.js 中
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        // 通过抽象语法树来检查模板中是否存在错误表达式
        detectErrors(compiled.ast, warn)
      }
      // 将收集到的错误(errors)和提示(tips)添加到 compiled 上并返回 compiled
      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
76
77
78
79
80
81
82
83
84
85
86
87
88
89

​ 我们对代码的注释都放在了上面的代码中,通过阅读源码,我们可以总结出,compile 函数主要做了以下几件事:

​ (1)、生成最终编译器选项 finalOptions

​ (2)、对错误的收集

​ (3)、调用 baseCompile 编译模板

说明:关于baseOptions,我们后面小节会详细说明。

# 4.3 baseCompile

​ 接下来我们继续来看 baseCompile 的定义,源码如下:

​ 源码目录:src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 使用 parse 函数将模板解析为 AST
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化 AST 树
    optimize(ast, options)
  }
  // 根据给定的AST生成目标平台的代码
  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

baseCompile 主要就是执行了如下几个逻辑:

​ (1) 解析模板字符串生成 AST

const ast = parse(template.trim(), options)
1

​ (2) 优化语法树

optimize(ast, options)
1

​ (3) 生成代码

const code = generate(ast, options)
1

说明:关于parseoptimizegenerate,我们后面章节会详细说明。

# 4.4 baseOptions

​ 通过前面对compile 函数的分析,我们知道 baseOptionscreateCompiler 函数的形参,也就是在 src/platforms/web/compiler/index.js 文件中调用 createCompiler 传递过来的参数:

​ 源码目录: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

​ 从源码可以知道 baseOptions 是通过 import 导入的,源码如下:

​ 源码目录:src/platforms/web/compiler/options.js

/* @flow */

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, // 标志是html
  modules, // 为虚拟dom添加staticClass,classBinding,staticStyle,styleBinding,for,alias,iterator1,iterator2,addRawAttr ,type ,key, ref,slotName或者slotScope或者slot,component或者inlineTemplate ,plain,if ,else,elseif 属性
  directives, // 为虚拟dom添加 model ,text ,html 方法
  isPreTag, // 通过给定的标签名字检查标签是否是 pre 标签
  isUnaryTag, // 检测给定的标签是否是一元标签
  mustUseProp, // 检测一个属性在标签中是否要使用 props 进行绑定
  canBeLeftOpenTag, // 检测一个标签是否是那些虽然不是一元标签,但却可以自己补全并闭合的标签。比如 p 标签是一个双标签,你需要这样使用 <p>Some content</p>,但是你依然可以省略闭合标签,直接这样写:<p>Some content,且浏览器会自动补全
  isReservedTag, // 检查给定的标签是否是保留的标签
  getTagNamespace, // 获取元素(标签)的命名空间
  staticKeys: genStaticKeys(modules) // 根据编译器选项的 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

​ 关于 baseOptions 的每一个属性的详细说明我们已在在代码中用注释的方式给出了,具体每一项是怎么来的,最终生成怎样的一个数据类型,有兴趣的同学可以自己研究一下。

说明:关于 baseOptions 每一项的具体怎么生成的,我们在实际案例中用到时,再来做补充分析。

# 5. 总结

编译整体流程