vue源码分析(八) 编译之整体流程
# 1. 概述
模板到真实 DOM
渲染的过程,中间有一个环节是把模板编译成 render
函数,这个过程我们把它称作编译。
Vue.js
提供了 2
个版本,一个是 Runtime + Compiler
的,一个是 Runtime only
的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack
的 vue-loader
事 先把模板编译成 render
函数。
# 2. 逻辑流程
当我们使用 Runtime + Compiler
的 Vue.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)
}
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 }
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
}
})
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)
}
}
}
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 {
/ * * /
}
}
2
3
4
5
6
7
8
9
10
11
至此我们总算找到了 compileToFunctions
的最终定义,它接收 3 个参数、编译模板 template
,编译配置 options
和 Vue
实例 vm
。
其中,核心的编译过程就一行代码:
源码目录:src/compiler/to-function.js
const compiled = compile(template, options)
compile
函数在执行 createCompileToFunctionFn
的时候作为参数传入,它是 createCompiler
函数中定义的 compile
函数。
源码目录:src/compiler/create-compiler.js
compile
函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:
const compiled = baseCompile(template.trim(), finalOptions)
baseCompile
在执行createCompilerCreator
方法时作为参数传入,如下:
源码目录:src/compiler/index.js
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
/ * * /
}
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)
}
}
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
: 默认flase
,IE
在属性值中编码换行,而其他浏览器则不会
shouldDecodeNewlinesForHref
:默认 true
,chrome
在a[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)
}
}
}
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
}
})
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)
(2) 优化语法树
optimize(ast, options)
(3) 生成代码
const code = generate(ast, options)
说明:关于parse
、optimize
、generate
,我们后面章节会详细说明。
# 4.4 baseOptions
通过前面对compile
函数的分析,我们知道 baseOptions
是 createCompiler
函数的形参,也就是在 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 }
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 选项生成一个静态键字符串
}
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
每一项的具体怎么生成的,我们在实际案例中用到时,再来做补充分析。