Vue Source Learning (7) Compiler Principle

The previous six blogs have helped us basically understand the concepts and principles of the responsive principle of vue, Data drive, component and component update, and the part about component and component update, we are based on the existing render function, that is to say, we already have vnode, and then discuss how vnode renders and updates.

In this blog, we will go back and see how the compiler of vue turns template into render function.

** What are the benefits of understanding the compile process? I personally think it can help us have a better understanding of the principles of some of vue’s own syntactic sugar. **

Since there are many details of Compiler, there is probably a conclusion and impression. Many of them will not go into detail about the syntax processing of vue itself, but will only use slot as an example.

Compiler entry

For the compiler entry point, vue does a series of currying to finally return the compile function.

This process is to pre-process the configuration related to the platform. For example, the compile of web compile and ssr will have some different configurations, and the platform will not change during the compile process, so these differences are first curried. Encapsulated in function closures, so that there is no need to judge the platform again and again when different components execute the compiler.

Currying

If you want to see how it is curried, you can take a look at this section. Not looking at it will not affect our understanding of the compile process.

When we use Vue.js with Runtime + Compiler, its entry is src/platforms/web/entry-runtime-with-compiler.js, take a look at its definition of the $mount function:

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

This function logic has been analyzed before, and the entry point for compile is here:

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

The compileToFunctions method is to compile the template template to generate render and staticRenderFns, which are defined in 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 }

You can see that the’compileToFunctions’ method is actually the return value of the’createCompiler ‘method, which receives a compile configuration parameter. Next, let’s look at the definition of the’createCompiler’ method, in’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
}
})

The createCompiler method is actually returned by calling the createCompilerCreator method. The parameter passed by this method is a function. The real compile process is executed in this baseCompile function. Then what is the createCompilerCreator? Its definition is in 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)
}
}
}

You can see that the method returns a’createCompiler ‘function, which receives a’baseOptions’ parameter, returns an object, including the’compile ‘method properties and’compileToFunctions’ properties, this’compileToFunctions’ corresponds to the’compileToFunctions’ method called by the ‘$mount’ function, which is the return value of the call to the’createCompileToFunctionFn ‘method. Let’s take a look at the’createCompileToFunctionFn’ method, which is defined in’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)
}
}

So far we have finally found the final definition of’compileToFunctions’, which takes 3 parameters, compile template template, compile configuration options and Vue instance vm. The core compile process is just one line of code:

1
const compiled = compile(template, options)

The’compile ‘function is passed as a parameter when executing’createCompileToFunctionFn’. It is the’compile ‘function defined in the’createCompiler’ function, as follows:

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
}

The logic of the’compile 'function is to process the configuration parameters first, and the actual execution of the compile process is one line of code:

1
const compiled = baseCompile(template, finalOptions)

'BaseCompile ‘is passed as a parameter when executing the’createCompilerCreator’ method

baseCompile

This function is the function that we actually compile to convert the template into 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
}
})

It is divided into three steps:

  • Convert template to ast tree
  • Optimize the ast tree, add some properties for some nodes for later rendering optimization, such as some nodes that will definitely not change plus a property indicating that it is a static node.
  • Generate code based on the optimized ast tree, which can be used to generate vnode

Let’s see the compile process through an example

parse

The compile process first parses the template and generates an AST. It is a Syntax Tree, which is a tree representation of the abstract syntax structure of the source code. In many compile technologies, such as babel compile ES6 code, it will be generated as AST.

This process is more complex, it will use a lot of Regular Expression to parse the string, if the regular is not very understanding, it is recommended to go to the knowledge of Regular Expression. In order to visually demonstrate the process of’parse ', let’s look at an example:

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>

After the’parse 'process, the generated AST is as follows:

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'}
]
]
}]
}

It can be seen that the generated AST is a tree structure, and each node is an’ast element ‘. In addition to some of its own properties, it also maintains its parent-child relationship, such as’parent’ pointing to its parent node, and’children 'pointing to its All sub-nodes. First, we have some intuitive impressions of the AST, so let’s analyze how this AST is obtained.

optimize

When our template template after the parse process, the output will generate AST tree, then we need to optimize the tree, optimize the logic is far simpler than the parse logic, so it will be much easier to understand.

Why should there be an optimization process, because we know that Vue is Data driven and responsive, but not all of our templates are responsive, and there are many data that will never change after the first render, so this part The DOM generated by the data will not change, and we can skip their comparison in the’patch 'process.

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

The final step of compile is to convert the optimized AST tree into executable code.

For ease of understanding, let’s use the previous example:

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>

It compiles, executes’const code = generate (ast, options) ‘, and the generated’render’ code string is as follows:

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()
}

The _c function is defined in 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
}

As the name implies, _c executes createElement to create a VNode, while _l renderList renders a list; _v createTextVNode creates a text VNode; _e creates an empty VNode for createEmptyVNode.

Source code analysis

Through the example just now, we can have a preliminary understanding of the entire compile process and the results of each step.

If you are interested, you can take a look at the source code of vue. Since there is a lot of source code, here is just a brief analysis.

parse

First, let’s take a look at the definition of’parse ‘, in’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
}

The code of’parse 'function is very long, and it is not good to post it again to understand colleagues. I will first split it into pseudocode so that colleagues can have a general understanding of the overall process. Next, we will decompose and analyze the role of each pseudocode.

From

Corresponding pseudocode:

1
getFnsAndConfigFromOptions(options)

The input of’parse ‘function is’template’ and’options’, and the output is the root node of AST. ‘template’ is our template string, and’options’ are actually some platform-related configurations, which are defined in’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)
}

These properties and methods are placed in the’platforms’ directory because they are implemented differently on different platforms (web and weex).

We represent this process with the pseudocode getFnsAndConfigFromOptions, whose actual code is as follows:

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

These methods and configurations are required for subsequent analysis, and we don’t need to care about their specific functions. Let’s look back first.

Analysis

Corresponding pseudocode:

1
parseHTML(template, options)

The parsing of the template is mainly through the parseHTML function, which is defined in 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()
}
}
}

Since the logic of’parseHTML ‘is also very complex, so I also used the pseudocode way of expression, the overall logic is to loop parsing’template’, using regular to do a variety of matching, for different cases were treated differently, until the entire template is parsed.
During the matching process, the’advance 'function will be used to continuously advance the entire template string until the end of the string.

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

The entire matching process is divided into the following steps:

  • Find the position of the first left angle bracket. If it is 0, it indicates that it is a label and enters the processing flow of the label. If not, it indicates that the angle bracket is in a certain string and enters the handlePlainTextElement () process
  • If it is a label, determine whether it is a comment node, if it is a comment node, skip the comment part
  • if it is doctype, skip
  • if it is a closing tag, i.e.
, advance the tag length and process the closing tag
  • If it is the start label, the forward label length, perform handleStartTag (), this function first creates a ast node out, the second is to push the current label and assign it to lastTag, why push the stack? It is to show that all labels are closed if the same label pops up from the stack when the closed label is processed in the previous step, and finally the stack is empty after the html compile is completed.
  • Of course, this process is a very simple introduction, which hides a lot of details. If you are interested, you can take a look at the source code for yourself.

    optimize

    Take a look at the definition of the’optimize ‘method, in’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 : '')
    )
    }

    We can optimize some AST nodes into static nodes in the compile stage, so the whole process of’optimize ‘actually does two things,’ markStatic (root) ‘marks the static node,’ markStaticRoots (root, false) 'marks the static root.

    Mark static node

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

    First execute node.static = isStatic (node)

    'isStatic ‘is a judgment on whether an AST element node is static. If it is an expression, it is non-static; if it is plain text, it is static; for an ordinary element, if it has a pre attribute, then it uses the’v-pre’ directive, which is static, otherwise the following conditions must be met at the same time: no’v-if ‘,’ v-for ‘, no other directives (excluding’v-once’), non-built-in components, platform-reserved labels, non-template tags with’v-for ’ Direct sub-node, the’key 'of all attributes of the node satisfies the static key; if these are satisfied, then this AST node is a static node.

    If this node is an ordinary element, then traverse all its children and recursion executes markStatic. Because all elseif and else nodes are not in children, if the ifConditions of the node is not null, then traverse ifConditions to get the block in all conditions, which is their corresponding AST node, and recursion executes markStatic. During these recursions, whenever a sub-node is not static, the static of its parent becomes false.

    Mark static 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
    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)
    }
    }
    }
    }

    The second parameter of markStaticRoots is isInFor. For nodes that are already static or v-once, node.staticInFor = isInFor.
    Then there is the judgment logic for’staticRoot ‘. From the comments, we can see that for nodes eligible to be’staticRoot’, in addition to being a static node themselves, they must satisfy the need to have’children ‘, and’children’ cannot be just a text node, otherwise the benefits of marking it as a static root node are very small.

    Next, as with the logic for marking static nodes, iterate over’children ‘and’ifConditions’, and recursion executes’markStaticRoots’.

    codegen

    The’generate ‘function is defined in’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
    }
    }

    The’generate ‘function first generates’code’ with’genElement (ast, state) ‘, and then wraps’code’ with’with (this) {return ${code}}} ‘. Here’state’ is an instance of’CodegenState ‘, which we will introduce later when we use it. Let’s take a look at’genElement’ first:

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

    Basically, it is to determine the attributes of the current AST element node to perform different code generation functions.

    slot

    The above source code analysis part omits a lot of details, some are related to html parsing, such as parseHTML function only writes pseudo-code, some are related to vue syntax sugar processing, such as codegen only writes genIF, genSlot, what exactly does not say, here we use slot as an example, others need you can see for yourself.

    Let’s start with compile. We know that compile occurs when’vm. $mount 'is called, so the order of compile is to compile the parent component first, and then compile the child components.

    First compile the parent component. In the’parse ‘stage, the’processSlot’ will be executed to process the’slot ‘, which is defined in’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)
    }
    }
    }
    }

    When it parses to the’slot ‘attribute on the label, the corresponding AST will be given.
    The element node adds the’slotTarget ‘attribute, and then in the’codegen’ stage, the’slotTarget ‘will be processed in’genData’, and the relevant code is in’src/compiler/codegen/index.js’:

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

    Will add a’slot ‘property to’data’ and point to’slotTarget ', which will be used later. In our example, the code generated by the parent component is as follows:

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

    Next compile subcomponent, also in the’parser ‘stage will execute the’processSlot’ processing function, which is defined in’src/compiler/parser/index.js’:

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

    When the’slot ‘tag is encountered, the’slotName’ attribute will be added to the corresponding AST element node, and then in the’codegen ‘phase, it will determine if the current AST element node is a’slot’ tag, then execute the’genSlot ‘function, which is defined in’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 + ')'
    }

    Let’s not consider the case of’attrs’ and’v-bind ‘on the’slot’ tag for now, so the code it generates is actually only:

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

    The’slotName ‘here is taken from the property corresponding to the AST element node, the default is’default’, and’children ‘corresponds to the content wrapped in the beginning and closing tags of the’slot’. Take a look at the code finally generated by the subcomponent of our example, as follows:

    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 ("default content") ]) ], 2),
    _c('footer',[_t("footer")],2)
    ]
    )
    }

    In the compile section, we learned that the _t function corresponds to the renderSlot method, which is defined in 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
    }
    }

    The parameter “name” of “render-slot” represents the slot name “slotName”, and “fallback” represents the “vnode” array generated by the default content of the slot. Ignore “scoped-slot” for now, and only look at the default slot logic. If “this. $slot [name]” has a value, return its corresponding “vnode” array, otherwise return “fallback”. So where did this. $slot "come from? We know that the’init ‘timing of the subcomponent is when the parent component executes the’patch’ process, and the parent component has already compiled at this time. And the subcomponent will execute the’initRender ‘function during the’init’ process, and get the’vm. $slot ‘when the’initRender’ is performed, and the relevant code is in’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’ is returned by executing’resolveSlots (options._renderChildren, renderContext) ‘, which is defined in’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
    }

    The resolveSlots method takes 2 parameters. The first parameter chilren corresponds to the children of the parent vnode, which in our case is the contents of the wraps < app-layout > and . The second parameter context is the context of the parent vnode, which is the vm instance of the parent component.

    The logic of the resolveSlots function is to traverse the chilren, get the data of each child, and then get the slot name through data.slot, which is the data.slot set by the compile parent component in the codegen stage. Then add’child ‘to’slots’ with the slot name’key ‘. If’data.slot’ does not exist, it is the content of the default slot, and add the corresponding’child ‘to’slots.defaults’. This gets the entire’slots’, which is an object, ‘key’ is the slot name, and’value ‘is an array of type’vnode’, because it can have multiple slots with the same name.

    In this way, we get the’vm. $slots’, return to the’renderSlot ‘function,’ const slotNodes = this. $slots [name] ‘, and we can also get the corresponding’vnode’ array according to the slot name. The’vnode 'in this array is created in the parent component, so that the content of the child component slot is replaced in the parent group.

    The corresponding’slot ‘is rendered as’vnodes’, and the’children ‘of the’vnode’ are rendered as the current component. The subsequent rendering process has been analyzed before and will not be repeated.