Vue源码学习(七)Compiler原理

前面的六篇博客已经帮助我们基本搞清楚了,vue的响应式原理,数据驱动,组件化和组件更新等概念和原理,而其中关于组件化和组件更新的部分,我们是在已有render函数的基础上来讲的,也就是说我们已经有了vnode,再去讨论的vnode如何渲染更新的。

这篇博客我们就回过头来看看vue的compiler是如何将template变成render函数的。

了解编译的流程有什么好处呢?我个人觉得可以帮助我们对于一些vue自身的语法糖的原理有个更好的理解。

由于Compiler的细节非常多,所以大概有个结论和印象即可,其中很多关于vue自身的语法处理也不会细讲,只会用slot来做个例子。

Compiler入口

对于compiler的入口,vue做了一系列柯里化的处理,来最终返回编译函数。

这样处理是为了将与平台有关的配置进行预处理,比如web编译和ssr的编译会有些许配置上的不同,而平台在编译过程中又不会发生改变,所以先通过柯里化将这些不同封装在函数闭包中,这样在不同组件执行compiler时就不需要一次次判断平台了。

柯里化

想要看看是怎么柯里化的,可以看看这一小节,不看也不影响我们理解编译的流程。

当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js,看一下它对 $mount 函数的定义:

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
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)

/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}

这段函数逻辑之前分析过,关于编译的入口就是在这里:

1
2
3
4
5
6
7
8
const { render, staticRenderFns } =  compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns,它的定义在 src/platforms/web/compiler/index.js 中:

1
2
3
4
5
6
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

可以看到 compileToFunctions 方法实际上是 createCompiler 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler 方法的定义,在 src/compiler/index.js 中:

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

createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,那么 createCompilerCreator 又是什么呢,它的定义在 src/compiler/create-compiler.js 中:

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
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}

if (options) {
// 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]
}
}
}

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

return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}

可以看到该方法返回了一个 createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,我们接下来看一下 createCompileToFunctionFn 方法,它的定义在 src/compiler/to-function/js 中:

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
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)

return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn

/* istanbul ignore if */
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
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}

// compile
const compiled = compile(template, options)

// check compilation errors/tips
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach(msg => tip(msg, vm))
}
}

// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
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)
}
}

至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

1
const compiled = compile(template, options)

compile 函数在执行 createCompileToFunctionFn 的时候作为参数传入,它是 createCompiler 函数中定义的 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
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}

if (options) {
// 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]
}
}
}

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

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

1
const compiled = baseCompile(template, finalOptions)

baseCompile 在执行 createCompilerCreator 方法时作为参数传入

baseCompile

这个函数就是我们实际去进行编译将template转化为render的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

它分为三个步骤:

  • 将template转化为ast树
  • 对ast树进行优化,为一些节点添加一些属性用于后面的渲染优化,如一些肯定不会发生变化的节点加上一个属性表明它是静态节点。
  • 根据优化后的ast树生成代码,这个代码就可以用来生成vnode

通过一个例子来看编译的流程

parse

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

这个过程是比较复杂的,它会用到大量正则表达式对字符串解析,如果对正则不是很了解,建议先去补习正则表达式的知识。为了直观地演示 parse 的过程,我们先来看一个例子:

1
2
3
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

经过 parse 过程后,生成的 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
48
49
50
51
52
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
]
]
}]
}

可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。

optimize

当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,optimize 的逻辑是远简单于 parse 的逻辑,所以理解起来会轻松很多。

为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。

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
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}

codegen

编译的最后一步就是把优化后的 AST 树转换成可执行的代码。

为了方便理解,我们还是用之前的例子:

1
2
3
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}

这里的 _c 函数定义在 src/core/instance/render.js 中。

1
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

而 _l、_v 定义在 src/core/instance/render-helpers/index.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}

顾名思义,_c 就是执行 createElement 去创建 VNode,而 _l 对应 renderList 渲染列表;_v 对应 createTextVNode 创建文本 VNode;_e 对于 createEmptyVNode创建空的 VNode。

源码分析

通过刚才的例子,我们可以对整个编译的流程以及每一步的结果有个初步的了解。

如果有兴趣可以看下vue的源码,由于源码非常多,这里只是简单分析下。

parse

首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:

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
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
getFnsAndConfigFromOptions(options)

parseHTML(template, {
// options ...
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs)
processElement(element)
treeManagement()
},

end () {
treeManagement()
closeElement()
},

chars (text: string) {
handleText()
createChildrenASTOfText()
},
comment (text: string) {
createChildrenASTOfComment()
}
})
return astRootElement
}

parse 函数的代码很长,贴一遍对同学的理解没有好处,我先把它拆成伪代码的形式,方便同学们对整体流程先有一个大致的了解。接下来我们就来分解分析每段伪代码的作用。

从 options 中获取方法和配置

对应伪代码:

1
getFnsAndConfigFromOptions(options)

parse 函数的输入是 template 和 options,输出是 AST 的根节点。template 就是我们的模板字符串,而 options 实际上是和平台相关的一些配置,它的定义在 src/platforms/web/compiler/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
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,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}

这些属性和方法之所以放到 platforms 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。

我们用伪代码 getFnsAndConfigFromOptions 表示了这一过程,它的实际代码如下:

1
2
3
4
5
6
7
8
9
10
11
warn = options.warn || baseWarn

platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no

transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

delimiters = options.delimiters

这些方法和配置都是后续解析时候需要的,可以不用去管它们的具体作用,我们先往后看。

解析 HTML 模板

对应伪代码:

1
parseHTML(template, options)

对于 template 模板的解析主要是通过 parseHTML 函数,它的定义在 src/compiler/parser/html-parser 中:

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
export function parseHTML (html, options) {
let lastTag
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)){
let textEnd = html.indexOf('<')
if (textEnd === 0) {
if(matchComment) {
advance(commentLength)
continue
}
if(matchDoctype) {
advance(doctypeLength)
continue
}
if(matchEndTag) {
advance(endTagLength)
parseEndTag()
continue
}
if(matchStartTag) {
parseStartTag()
handleStartTag()
continue
}
}
handleText()
advance(textLength)
} else {
handlePlainTextElement()
parseEndTag()
}
}
}

由于 parseHTML 的逻辑也非常复杂,因此我也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。
在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。

1
2
3
4
function advance (n) {
index += n
html = html.substring(n)
}

整个匹配过程分为以下几步:

  • 找到第一个左尖括号的位置,如果是0,表明这是个标签的,进入标签的处理流程,如果不是,说明这个尖括号处在某个字符串中,进入handlePlainTextElement()流程
  • 如果是个标签,判断是否是注释节点,如果是注释节点,跳过注释部分
  • 如果是doctype,跳过
  • 如果是结束标签,也就是说
这种,则前进标签长度,并处理闭合标签
  • 如果是开始标签,则前进标签长度,执行handleStartTag(),这个函数首先是创建一个ast的node出来,第二就是把当前标签压栈,并赋值给lastTag,为什么要压栈呢?是为了在上一步处理闭合标签时如果标签相同从栈里弹出来,最终html编译完成之后栈为空才说明所有标签闭合的。
  • 当然这个流程是一个非常简单的介绍,里面隐藏了非常多的细节,有兴趣可以自己去看看源码。

    optimize

    来看一下 optimize 方法的定义,在 src/compiler/optimizer.js 中:

    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
    /**
    * Goal of the optimizer: walk the generated template AST tree
    * and detect sub-trees that are purely static, i.e. parts of
    * the DOM that never needs to change.
    *
    * Once we detect these sub-trees, we can:
    *
    * 1. Hoist them into constants, so that we no longer need to
    * create fresh nodes for them on each re-render;
    * 2. Completely skip them in the patching process.
    */
    export function optimize (root: ?ASTElement, options: CompilerOptions) {
    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)
    }

    function genStaticKeys (keys: string): Function {
    return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
    (keys ? ',' + keys : '')
    )
    }

    我们在编译阶段可以把一些 AST 节点优化成静态节点,所以整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, 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
    42
    43
    44
    45
    46
    47
    48
    function markStatic (node: 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
    if (
    !isPlatformReservedTag(node.tag) &&
    node.tag !== 'slot' &&
    node.attrsMap['inline-template'] == null
    ) {
    return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
    const child = node.children[i]
    markStatic(child)
    if (!child.static) {
    node.static = false
    }
    }
    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
    }
    }
    }
    }
    }

    function isStatic (node: ASTNode): boolean {
    if (node.type === 2) { // expression
    return false
    }
    if (node.type === 3) { // text
    return 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) &&
    Object.keys(node).every(isStaticKey)
    ))
    }

    首先执行 node.static = isStatic(node)

    isStatic 是对一个 AST 元素节点是否是静态的判断,如果是表达式,就是非静态;如果是纯文本,就是静态;对于一个普通元素,如果有 pre 属性,那么它使用了 v-pre 指令,是静态,否则要同时满足以下条件:没有使用 v-if、v-for,没有使用其它指令(不包括 v-once),非内置组件,是平台保留的标签,非带有 v-for 的 template 标签的直接子节点,节点的所有属性的 key 都满足静态 key;这些都满足则这个 AST 节点是一个静态节点。

    如果这个节点是一个普通元素,则遍历它的所有 children,递归执行 markStatic。因为所有的 elseif 和 else 节点都不在 children 中, 如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block,也就是它们对应的 AST 节点,递归执行 markStatic。在这些递归过程中,一旦子节点有不是 static 的情况,则它的父节点的 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
    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.
    if (node.static && node.children.length && !(
    node.children.length === 1 &&
    node.children[0].type === 3
    )) {
    node.staticRoot = true
    return
    } else {
    node.staticRoot = false
    }
    if (node.children) {
    for (let i = 0, l = node.children.length; i < l; i++) {
    markStaticRoots(node.children[i], isInFor || !!node.for)
    }
    }
    if (node.ifConditions) {
    for (let i = 1, l = node.ifConditions.length; i < l; i++) {
    markStaticRoots(node.ifConditions[i].block, isInFor)
    }
    }
    }
    }

    markStaticRoots 第二个参数是 isInFor,对于已经是 static 的节点或者是 v-once 指令的节点,node.staticInFor = isInFor。
    接着就是对于 staticRoot 的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot 的节点,除了本身是一个静态节点外,必须满足拥有 children,并且 children 不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。

    接下来和标记静态节点的逻辑一样,遍历 children 以及 ifConditions,递归执行 markStaticRoots。

    codegen

    generate 函数的定义在 src/compiler/codegen/index.js 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export function generate (
    ast: ASTElement | void,
    options: CompilerOptions
    ): CodegenResult {
    const state = new CodegenState(options)
    const code = ast ? genElement(ast, state) : '_c("div")'
    return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
    }
    }

    generate 函数首先通过 genElement(ast, state) 生成 code,再把 code 用 with(this){return ${code}}} 包裹起来。这里的 state 是 CodegenState 的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 genElement:

    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
    export function genElement (el: ASTElement, state: CodegenState): string {
    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) {
    return genChildren(el, state) || 'void 0'
    } 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 {
    const data = el.plain ? undefined : genData(el, state)

    const children = el.inlineTemplate ? null : genChildren(el, state, true)
    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
    }
    }

    基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数。

    slot

    上面的源码分析部分省略了很多很多的细节,有的是html解析相关的,如parseHTML函数只写了伪代码,有的是vue语法糖处理相关的,如codegen只写了genIF,genSlot,具体做了什么没有说,这里我们就用slot举个例子,其他的有需要大家可以自己看。

    还是先从编译说起,我们知道编译是发生在调用 vm.$mount 的时候,所以编译的顺序是先编译父组件,再编译子组件。

    首先编译父组件,在 parse 阶段,会执行 processSlot 处理 slot,它的定义在 src/compiler/parser/index.js 中:

    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
    function processSlot (el) {
    if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name')
    if (process.env.NODE_ENV !== 'production' && el.key) {
    warn(
    `\`key\` does not work on <slot> because slots are abstract outlets ` +
    `and can possibly expand into multiple elements. ` +
    `Use the key on a wrapping element instead.`
    )
    }
    } else {
    let slotScope
    if (el.tag === 'template') {
    slotScope = getAndRemoveAttr(el, 'scope')
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && slotScope) {
    warn(
    `the "scope" attribute for scoped slots have been deprecated and ` +
    `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
    `can also be used on plain elements in addition to <template> to ` +
    `denote scoped slots.`,
    true
    )
    }
    el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
    warn(
    `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
    `(v-for takes higher priority). Use a wrapper <template> for the ` +
    `scoped slot to make it clearer.`,
    true
    )
    }
    el.slotScope = slotScope
    }
    const slotTarget = getBindingAttr(el, 'slot')
    if (slotTarget) {
    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
    // preserve slot as an attribute for native shadow DOM compat
    // only for non-scoped slots.
    if (el.tag !== 'template' && !el.slotScope) {
    addAttr(el, 'slot', slotTarget)
    }
    }
    }
    }

    当解析到标签上有 slot 属性的时候,会给对应的 AST
    元素节点添加 slotTarget 属性,然后在 codegen 阶段,在 genData 中会处理 slotTarget,相关代码在 src/compiler/codegen/index.js 中:

    1
    2
    3
    if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
    }

    会给 data 添加一个 slot 属性,并指向 slotTarget,之后会用到。在我们的例子中,父组件最终生成的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    with(this){
    return _c('div',
    [_c('app-layout',
    [_c('h1',{attrs:{"slot":"header"},slot:"header"},
    [_v(_s(title))]),
    _c('p',[_v(_s(msg))]),
    _c('p',{attrs:{"slot":"footer"},slot:"footer"},
    [_v(_s(desc))]
    )
    ])
    ],
    1)}

    接下来编译子组件,同样在 parser 阶段会执行 processSlot 处理函数,它的定义在 src/compiler/parser/index.js 中:

    1
    2
    3
    4
    5
    6
    function processSlot (el) {
    if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name')
    }
    // ...
    }

    当遇到 slot 标签的时候会给对应的 AST 元素节点添加 slotName 属性,然后在 codegen 阶段,会判断如果当前 AST 元素节点是 slot 标签,则执行 genSlot 函数,它的定义在 src/compiler/codegen/index.js 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function genSlot (el: ASTElement, state: CodegenState): string {
    const slotName = el.slotName || '"default"'
    const children = genChildren(el, state)
    let res = `_t(${slotName}${children ? `,${children}` : ''}`
    const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}`
    const bind = el.attrsMap['v-bind']
    if ((attrs || bind) && !children) {
    res += `,null`
    }
    if (attrs) {
    res += `,${attrs}`
    }
    if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
    }
    return res + ')'
    }

    我们先不考虑 slot 标签上有 attrs 以及 v-bind 的情况,那么它生成的代码实际上就只有:

    1
    2
    3
    const slotName = el.slotName || '"default"'
    const children = genChildren(el, state)
    let res = `_t(${slotName}${children ? `,${children}` : ''}`

    这里的 slotName 从 AST 元素节点对应的属性上取,默认是 default,而 children 对应的就是 slot 开始和闭合标签包裹的内容。来看一下我们例子的子组件最终生成的代码,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    with(this) {
    return _c('div',{
    staticClass:"container"
    },[
    _c('header',[_t("header")],2),
    _c('main',[_t("default",[_v("默认内容")])],2),
    _c('footer',[_t("footer")],2)
    ]
    )
    }

    在编译章节我们了解到,_t 函数对应的就是 renderSlot 方法,它的定义在 src/core/instance/render-heplpers/render-slot.js 中:

    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
    /**
    * Runtime helper for rendering <slot>
    */
    export function renderSlot (
    name: string,
    fallback: ?Array<VNode>,
    props: ?Object,
    bindObject: ?Object
    ): ?Array<VNode> {
    const scopedSlotFn = this.$scopedSlots[name]
    let nodes
    if (scopedSlotFn) { // scoped slot
    props = props || {}
    if (bindObject) {
    if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
    warn(
    'slot v-bind without argument expects an Object',
    this
    )
    }
    props = extend(extend({}, bindObject), props)
    }
    nodes = scopedSlotFn(props) || fallback
    } else {
    const slotNodes = this.$slots[name]
    // warn duplicate slot usage
    if (slotNodes) {
    if (process.env.NODE_ENV !== 'production' && slotNodes._rendered) {
    warn(
    `Duplicate presence of slot "${name}" found in the same render tree ` +
    `- this will likely cause render errors.`,
    this
    )
    }
    slotNodes._rendered = true
    }
    nodes = slotNodes || fallback
    }

    const target = props && props.slot
    if (target) {
    return this.$createElement('template', { slot: target }, nodes)
    } else {
    return nodes
    }
    }

    render-slot 的参数 name 代表插槽名称 slotName,fallback 代表插槽的默认内容生成的 vnode 数组。先忽略 scoped-slot,只看默认插槽逻辑。如果 this.$slot[name] 有值,就返回它对应的 vnode 数组,否则返回 fallback。那么这个 this.$slot 是哪里来的呢?我们知道子组件的 init 时机是在父组件执行 patch 过程的时候,那这个时候父组件已经编译完成了。并且子组件在 init 过程中会执行 initRender 函数,initRender 的时候获取到 vm.$slot,相关代码在 src/core/instance/render.js 中:

    1
    2
    3
    4
    5
    6
    export function initRender (vm: Component) {
    // ...
    const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
    const renderContext = parentVnode && parentVnode.context
    vm.$slots = resolveSlots(options._renderChildren, renderContext)
    }

    vm.$slots 是通过执行 resolveSlots(options._renderChildren, renderContext) 返回的,它的定义在 src/core/instance/render-helpers/resolve-slots.js 中:

    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
    /**
    * Runtime helper for resolving raw children VNodes into a slot object.
    */
    export function resolveSlots (
    children: ?Array<VNode>,
    context: ?Component
    ): { [key: string]: Array<VNode> } {
    const slots = {}
    if (!children) {
    return slots
    }
    for (let i = 0, l = children.length; i < l; i++) {
    const child = children[i]
    const data = child.data
    // remove slot attribute if the node is resolved as a Vue slot node
    if (data && data.attrs && data.attrs.slot) {
    delete data.attrs.slot
    }
    // named slots should only be respected if the vnode was rendered in the
    // same context.
    if ((child.context === context || child.fnContext === context) &&
    data && data.slot != null
    ) {
    const name = data.slot
    const slot = (slots[name] || (slots[name] = []))
    if (child.tag === 'template') {
    slot.push.apply(slot, child.children || [])
    } else {
    slot.push(child)
    }
    } else {
    (slots.default || (slots.default = [])).push(child)
    }
    }
    // ignore slots that contains only whitespace
    for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
    delete slots[name]
    }
    }
    return slots
    }

    resolveSlots 方法接收 2 个参数,第一个参数 chilren 对应的是父 vnode 的 children,在我们的例子中就是 <app-layout> 和 </app-layout> 包裹的内容。第二个参数 context 是父 vnode 的上下文,也就是父组件的 vm 实例。

    resolveSlots 函数的逻辑就是遍历 chilren,拿到每一个 child 的 data,然后通过 data.slot 获取到插槽名称,这个 slot 就是我们之前编译父组件在 codegen 阶段设置的 data.slot。接着以插槽名称为 key 把 child 添加到 slots 中,如果 data.slot 不存在,则是默认插槽的内容,则把对应的 child 添加到 slots.defaults 中。这样就获取到整个 slots,它是一个对象,key 是插槽名称,value 是一个 vnode 类型的数组,因为它可以有多个同名插槽。

    这样我们就拿到了 vm.$slots 了,回到 renderSlot 函数,const slotNodes = this.$slots[name],我们也就能根据插槽名称获取到对应的 vnode 数组了,这个数组里的 vnode 都是在父组件创建的,这样就实现了在父组替换子组件插槽的内容了。

    对应的 slot 渲染成 vnodes,作为当前组件渲染 vnode 的 children,之后的渲染过程之前分析过,不再赘述。