# 模板编译原理

vue提供了模板语法,允许我们声明式地描述状态和DOM之间的绑定关系,然后通过模板来生成真实DOM并将其呈现在用户界面上。在底层实现上,vue会将模板编译成虚拟DOM渲染函数。当应用内部的状态发生变化时,vue可以结合响应式系统,聪明地找出最小数量的组件进行重新渲染以及最少量地进行DOM操作。

在vue中创建Html并不是只有模板这一种途径,我们既可以手动写渲染函数来创建,也可以使用JSX来创建。渲染函数是创建HTML最原始的方法。模板最终会通过编译转换成渲染函数,渲染函数执行后,会得到一份vnode用于虚拟DOM渲染。所以模板编译其实是配合虚拟DOM进行渲染。

渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的vnode,然后使用这个vnode进行渲染。

# 将模板编译成渲染函数

先将模板解析成AST(抽象语法树),然后使用抽象语法树生成渲染函数。

TIP

由于静态节点不需要总是重新渲染,所以生成AST之后,生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍抽象语法树,给所有静态节点做一个标记,这样在虚拟DOM 中更新节点时,如果发现这个节点有标记就不会重新渲染它。

对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime 是一种性能损耗。而模板编译的目的仅仅是将template转化为render function,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime 时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template的过程,就是预编译。

所以模板编译大体分为三部分:

  • 将模板解析为AST--解析器
  • 遍历抽象语法树标记静态节点--优化器
  • 使用抽象语法树生成渲染函数--代码生成器

# 解析器

在解析器内部分成很多小解析器:包括过滤器解析器,文本解析器和HTML解析器,然后通过一条主线将这些解析器组装在一起。

  • 过滤器解析器

用来解析过滤器的

  • 文本解析器

用来解析带变量的文本

  • HTML解析器

解析模板,每当解析到HTML标签的开始位置,结束位置,文本或者注释,都会触发钩子函数,然后将相关信息通过参数传递出来。

TIP

主线上做的事情就是监听HTML解析器。每当触发钩子函数时,就生成一个对应的抽象语法树节点。生成抽象语法树前,会根据类型使用不同的方式生成不同抽象语法树。例如,如果是文本节点,就生成文本类型的AST当解析器不再触发钩子函数时,就说明所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。

# 解析器内部原理

钩子函数start有三个参数,分别是tag/attrs/unary。分别说明标签名,标签的属性和是否是自闭合标签。而文本节点的钩子函数chars和注释节点的钩子函数comment都只有一个参数,只有text。这是因为构建元素节点时需要知道标签名,属性和自闭合标识,而构建注释节点和文本节点只需要知道文本即可。

<div>
  <h1>
  我是林嘉恒
  </h1>
  <p>
  今年24岁
  </p>
</div>
1
2
3
4
5
6
7
8
function createASTElement ( // 构造元素类型的AST节点
  tag,
  attrs,
  parent
) {
  return {
    type: 1,
    tag: tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent: parent,
    children: []
  }
}

parseHTML(template, {
  start(tag, attrs, unary, start$1, end) {
    var element = createASTElement(tag, attrs, currentParent); // 在start钩子函数创建一个元素类型的ast节点
  }
  chars (text, start, end) { // 触发文本的钩子函数
    let element = { type: 3, text }
  }
  comment (text, start, end) { // 触发注释
    let element = { type: 3, text, isComment: true}
  }
})
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

TIP

这个模板被解析成AST过程需要注意的点:

  • 利用一个栈来记录层级关系
  • 基于HTML解析器的逻辑,我们可以在每次触发钩子函数start时,把当前构建的节点推入栈中;每当触发钩子函数end时就会从栈中弹出一个节点。
  • 这样就可以保证每当触发钩子函数start时,栈中最后一个节点就是当前正在构建的节点的父节点。
  • 模板中开始位置的一些空格,会触发文本节点的钩子函数,在钩子函数里会忽略这些空格。同时会在模板中将这些空格截取掉。
  • 解析到文本节点时,栈中最后一个节点就是其父节点,将文本节点添加到该父节点的子节点中。由于文本节点没有子节点所以文本节点不会被推入到栈中
  • 栈空时,就得到一个完整的带层级关系的抽象语法树。
  • 自闭合标签不需要入栈

# HTML解析器 运行原理

解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复上述过程,直到HTML模板被截成一个空字符串时循环结束。

解析器如何知道每轮循环应该截取哪些字符串的?

截取类型:

  • 开始标签:<div>
  • 结束标签:</div>
  • HTML注释:例如<!-- 我是注释 -->
  • DOCTYPE:例如<!DOCTYPE html>
  • 条件注释:例如<!-- [if !IE] --> 我是注释 <!-- ![endif]>
  • 文本
  1. 模版截取,只有以开始标签开头的才需要进行标签截取操作,先判断是否以<开头
  2. 判断是<开头后,还需要通过正则来确定具体标签类型
  3. 触发 start钩子函数需要几个参数,所以需要将标签名,属性以及自闭合标识解析出来
  4. 进一步解析属性和自闭合标识。每解析一个属性就截取一个属性,截取完剩下的html模版依然符合标签属性的正则,说明还有剩余属性需要处理。重复执行
  5. 截取结束标签跟开始标签一样,判断是否<开头,然后进一步判断剩余HTML模版的开始位置是否符合正则表达式即可。
  6. 如果HTML模板中第一个字符不是<就一定是文本,只要找到下一个<位置,就可以截取到文本
  7. 如果将 < 前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段类型,说明<是文本一部分。当判断出是属于文本一部分后,就要找到下一个<,并将其前面的文本截取出来并加到之前截取一板的文本后面
  8. 如何知道父元素 lastTag是谁呢,通过栈,该栈还有另外一个作用可以检测出标签是否正确闭合
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;

var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));

var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

var startTagClose = /^\s*(\/?)>/;

var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));

var comment = /^<!\--/;

var doctype = /^<!DOCTYPE [^>]+>/i;

// 以开始标签开始的模版
'<div></div>'.match(startTagOpen) // ['<div', 'div', groups: undefined, index: 0, input: "<div></div>"]

// 以结束标签开始的模版
'</div>'.match(endTag) // ["</div>", 'div', index: 0, input: '</div>']

var textEnd = html.indexOf('<');
if (textEnd === 0) {
  // 注释
  if (comment.test(html)) {
    var commentEnd = html.indexOf('-->');
    if (commentEnd >= 0) {
        if (options.shouldKeepComment) { // 在完整构建版本中的浏览器内编译时可用。保留注释
            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
        }
        advance(commentEnd + 3);
        continue
    }
  }

  // 条件注释
  if (conditionalComment.test(html)) {
    var conditionalEnd = html.indexOf(']>');

    if (conditionalEnd >= 0) {
        advance(conditionalEnd + 2);
        continue
    }
  }

  // Doctype:
  var doctypeMatch = html.match(doctype);
  if (doctypeMatch) {
    advance(doctypeMatch[0].length);
    continue
  }

  // End tag:
  var endTagMatch = html.match(endTag);
  if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length); // 模板截取
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
  }

  // Start tag:
  var startTagMatch = parseStartTag();
  if (startTagMatch) {
    handleStartTag(startTagMatch);
    if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
        advance(1);
    }
    continue // 这一轮解析工作已经完成,进行下一轮解析工作
  }
}

// 截取文本
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
    rest = html.slice(textEnd);
    while ( // 这里是为了防止 < 是文本的一部分,
        !endTag.test(rest) &&
        !startTagOpen.test(rest) &&
        !comment.test(rest) &&
        !conditionalComment.test(rest)
    ) {
        // < in plain text, be forgiving and treat it as text
        next = rest.indexOf('<', 1);
        if (next < 0) { break }
        textEnd += next;
        rest = html.slice(textEnd);
    }
    text = html.substring(0, textEnd);
}

if (textEnd < 0) {
    text = html; // 如果模板找不到<,就说明整个模板都是文本
}

if (text) {
    advance(text.length); // 直接将html设置为空了相当于
}

if (options.chars && text) {
    options.chars(text, index - text.length, index);
}

function advance (n) {
  index += n;
  html = html.substring(n);
}

function parseStartTag () {
  var start = html.match(startTagOpen);
  if (start) {
    var match = {
        tagName: start[1],
        attrs: [],
        start: index
    };
    advance(start[0].length);
    var end, attr;
    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        // 这里的意思:如果剩余html模版不符合开始标签结尾部分的特征,并且符合标签属性的特征,就进入循环中解析和截取操作
        attr.start = index;
        advance(attr[0].length);
        attr.end = index;
        match.attrs.push(attr);
        // 解析后结果:保存在attrs中
        // {
        //   tagName: 'div',
        //   attrs: [
        //   [' class="box"', 'class', '=', 'box', null, null],
        //   [' id="el"', 'id', '=', 'el', null, null]
        //   ]
        // }

        // 解析后:剩余 ></div>
        // 解析前:' class="box" id="el"></div>
    }
    if (end) {
        match.unarySlash = end[1]; // 自闭合标签解析后的该属性为/,非自闭合标签为空字符串
        advance(end[0].length);
        match.end = index;
        return match
    }
  }
}
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

纯文本内容元素处理?script/style/textarea元素叫做纯文本内容元素。解析他们的时候会把这三种标签内容当文本处理

  • 把文本截取出来并触发钩子函数chars,然后再将结束标签截取出来并触发钩子函数end
var last, lastTag; // lastTag表示父元素
while(html) {
    last = html;
    if (!lastTag || !isPlainTextElement(lastTag)) {
        // 前面分析的内容
    } else {
        // 纯文本内容处理逻辑
        var endTagLength = 0;
        var stackedTag = lastTag.toLowerCase();
        var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
        var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
            endTagLength = endTag.length;
            if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
                text = text
                    .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
                    .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
            }
            if (shouldIgnoreFirstNewline(stackedTag, text)) {
                text = text.slice(1);
            }
            if (options.chars) {
                options.chars(text);
            }
            return ''
        });
        index += html.length - rest$1.length;
        html = rest$1;
        parseEndTag(stackedTag, index - endTagLength, index);
    }
}
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

# 文本解析器

文本解析器的作用是解析文本。奇怪的是文本不是已经在HTML解析器中被解析出来了么?准确来说文本解析器是对HTML解析器解析出来的文本进行二次加工。因为文本分两种类型,一种是纯文本,另一种是带变量的文本。

在vue中我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否带变量的文本。如果是纯文本不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量的值。

在文本解析器中,第一步做的事情就是使用正则表达式来判断文本是否为带变量的文本,也就是检查文本中是否包含这样的语法。如果是纯文本,则直接返回undefined;如果是带变量的文本,再进行二次加工。

function parseText (
  text,
  delimiters
) {
  // defaultTagRE: /\{\{((?:.|\r?\n)+?)\}\}/g; delimiters 只在完整构建版本浏览器内编译使用,改变纯文本插入分隔符
  var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
  if (!tagRE.test(text)) { // 纯文本直接返回undefined了
    return
  }
  var tokens = [];
  var rawTokens = [];
  var lastIndex = tagRE.lastIndex = 0;
  var match, index, tokenValue;
  while ((match = tagRE.exec(text))) {
    index = match.index;
    // push text token
    if (index > lastIndex) { // 先把 {{ 前面的文本添加到tokens中
      rawTokens.push(tokenValue = text.slice(lastIndex, index));
      tokens.push(JSON.stringify(tokenValue));
    }
    // tag token
    var exp = parseFilters(match[1].trim());
    tokens.push(("_s(" + exp + ")"));
    rawTokens.push({ '@binding': exp });
    lastIndex = index + match[0].length; // 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
  }
  if (lastIndex < text.length) { // 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
    rawTokens.push(tokenValue = text.slice(lastIndex));
    tokens.push(JSON.stringify(tokenValue));
  }
  return {
    expression: tokens.join('+'), // 将数组里面的文本合成一个字符串
    tokens: rawTokens
  }
}
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

每当HTML解析器解析到文本时触发chars函数,并且解析得到文本。在chars函数中需要构建文本类型的AST,并将它添加到父节点的children属性中。如果是带变量的文本需要经过parseText二次加工。此时构建了一个带变量的文本类型AST并加到父节点children属性中。

如何解决带变量的文本呢:使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。如果变量后面有变量则重复这动作。这时有一个数组,数组元素和文本顺序一致,此时将数组元素用+连起来变成字符串,就可以得到最后结果

"Hello {{name}}"
// 解析后expression:
'Hello ' + _s(name)

// _s(x)就是toString别名
export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}
1
2
3
4
5
6
7
8
9
10
11
12

# 优化器

优化器的目标遍历AST,检测出所有静态子树(永远都不会发生变化的DOM节点)并打上标记。当AST中静态子树被打上标记后,每次重新渲染时就不需要为打上标记的静态节点创建新的虚拟节点,而是直接克隆已经存在的虚拟节点。静态子树是指的是那些在AST中永远都不会发生变化的节点

  • 标记静态子树有两点好处:
    • 每次重新渲染时,不需要为静态子树创建新节点;
    • 在虚拟DOM中打补丁patching的过程可以跳过。

TIP

每次重新渲染都会使用最新的状态生成一份全新的VNode与旧的VNode进行对比。而生成VNode的过程中,如果发现一个节点被标记为静态子树,那么除了首次渲染会生成节点之外,在重新渲染时并不会生成新的子节点树,而是克隆已存在的静态子树。

打补丁patch过程中,如果两个节点都是静态子树,就不需要进行对比与更新DOM的操作,直接跳过。因为静态子树是不可变的,不需要对比就知道不可能发生变化。

  • 优化器内部实现两个步骤:
    • AST中找出所有静态节点并打上标记
    • AST中找出所有静态根节点并打上标记
  • 落实到AST中静态节点指的是static属性为true的值
  • 如果一个节点下面所有子节点都是静态节点,并且父级是动态节点,那么它就是静态根节点。staticRoottrue
function optimize (root, options) {
  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);
}
1
2
3
4
5
6
7
8
9

# 找出所有静态节点并标记

递归处理 root根节点,先判断根节点是不是静态根节点,再用相同方式处理子节点。

function isDirectChildOfTemplateFor (node) {
  while (node.parent) {
    node = node.parent;
    if (node.tag !== 'template') {
      return false
    }
    if (node.for) {
      return true
    }
  }
  return false
}

function isStatic (node) {
  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)
  ))
}

function markStatic (node) {
  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 (var i = 0, l = node.children.length; i < l; i++) {
      var child = node.children[i];
      markStatic(child);
      if (!child.static) { // 做校对
        node.static = false;
      }
    }
    if (node.ifConditions) {
      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
        var block = node.ifConditions[i$1].block;
        markStatic(block);
        if (!block.static) {
          node.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
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
  • node.pre:如果元素节点使用了指令v-pre,可以直接断定是一个静态节点
  • 不能使用v-if/v-for/v-else指令
  • 不能是内置标签,也就是说标签名不能是slot/component
  • 不能是组件
  • 当前节点的父节点不能是带有v-fortemplate 标签
  • 节点中不存在动态节点才会有的属性
  • 不能使用动态绑定语法,也就是说标签上不能有以v-/@/:开头的属性。不包括v-for/v-if/v-else/v-else-if/v-once

'节点不存在动态节点才会有的属性'这里的意思是如果一个元素节点是静态节点,那么这个节点上的属性其实有范围。范围是type/tag/attrsList/attrsMap/plain/parent/children/attrs/staticClass/staticStyle。 递归从上到下依次标记,如果父节点被标记为静态节点后,子节点却为动态,这样就矛盾了。因为静态子树所有子节点都为静态节点。所以需要校验一下

# 找出所有静态根节点并标记

function markStaticRoots (node, isInFor) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor;
    }
    // 要使节点符合静态根节点的要求,必须有子节点
    // 这个子节点不能只有一个静态文本的子节点,否则优化成本超出效益
    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 (var i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for);
      }
    }
    if (node.ifConditions) {
      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
        markStaticRoots(node.ifConditions[i$1].block, isInFor);
      }
    }
  }
}
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
  • 跟找出静态节点过程类似。不一样的是如果一个节点被判断为静态根节点,不会继续向子级继续寻找。
  • 优化成本大于收益的,即使是静态根节点也不会标记

# 代码生成器

代码生成器是模板函数编译最后一步,作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。

<p title='Berwin' @click='c'>1</p>

// 生成的代码字符串
`with(this){return _c('p',{attrs: {"titile":"Berwin"}, on: {"clcik":c}}, [_v("1")]}`

//格式化后
with(this) {
  return _c(
  'p',
  {
    attrs:{"title":"Berwin"},
    on:{"click":c}
  },
  [_v("1")]
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TIP

渲染函数之所以可以生成vnode,是因为代码字符串中会有很多函数调用(如_c/_v),这些函数是虚拟DOM提供创建vnode的方法。vnode有很多种类型,不同的类型对应不同的创建方法,所以代码字符串中的_c/_v其实都是创建vnode的方法,只是创建的vnode的类型不同。例如,_c可以创建元素类型的vnode,而_v可以创建文本类型的vnode

代码字符串中的_c其实是createElement的别名。createElement是虚拟DOM中提供的方法,作用是创建虚拟节点,有三个参数:

  • 标签名
  • 一个包含模板相关属性的数据对象
  • 子节点列表

这也知道了渲染函数可以生成VNode的原因:渲染函数其实是执行了createElement,而createElement可以创建一个VNode

代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。会将字符串在with中返回给调用者。这样一个代码字符串最终导出到外界使用时,会将代码字符串放到函数里,这个函数叫做渲染函数

三种节点对应的创建方法与别名

类型 创建方法 别名
元素节点 createElement _c
文本节点 createTextNode _v
注释节点 createEmptyNode _e

# 代码生成器原理

  • 生成元素节点,其实就是生成一个_c的函数调用字符串
function genElement (el, state) {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre;
  }

  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 && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    var code;
    if (el.component) {
      code = genComponent(el.component, el, state);
    } else {
      var data;
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData$2(el, state);
      }

      var children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
    }
    // module transforms
    for (var i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code);
    }
    return code
  }
}

function genData$2 (el, state) {
  var data = '{';

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  var dirs = genDirectives(el, state);
  if (dirs) { data += dirs + ','; }

  // key
  if (el.key) {
    data += "key:" + (el.key) + ",";
  }
  // ref
  if (el.ref) {
    data += "ref:" + (el.ref) + ",";
  }
  if (el.refInFor) {
    data += "refInFor:true,";
  }
  // pre
  if (el.pre) {
    data += "pre:true,";
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += "tag:\"" + (el.tag) + "\",";
  }
  // module data generation functions
  for (var i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el);
  }
  // attributes
  if (el.attrs) {
    data += "attrs:" + (genProps(el.attrs)) + ",";
  }
  // DOM props
  if (el.props) {
    data += "domProps:" + (genProps(el.props)) + ",";
  }
  // event handlers
  if (el.events) {
    data += (genHandlers(el.events, false)) + ",";
  }
  if (el.nativeEvents) {
    data += (genHandlers(el.nativeEvents, true)) + ",";
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += "slot:" + (el.slotTarget) + ",";
  }
  // scoped slots
  if (el.scopedSlots) {
    data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
  }
  // component v-model
  if (el.model) {
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }
  // inline-template
  if (el.inlineTemplate) {
    var inlineTemplate = genInlineTemplate(el, state);
    if (inlineTemplate) {
      data += inlineTemplate + ",";
    }
  }
  data = data.replace(/,$/, '') + '}';
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {
    data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")";
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data);
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data);
  }
  return data
}

function genChildren (
  el,
  state,
  checkSkip,
  altGenElement,
  altGenNode
) {
  var children = el.children;
  if (children.length) {
    var el$1 = children[0];
    // optimize single v-for
    if (children.length === 1 &&
      el$1.for &&
      el$1.tag !== 'template' &&
      el$1.tag !== 'slot'
    ) {
      var normalizationType = checkSkip
        ? state.maybeComponent(el$1) ? ",1" : ",0"
        : "";
      return ("" + ((altGenElement || genElement)(el$1, state)) + normalizationType)
    }
    var normalizationType$1 = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0;
    var gen = altGenNode || genNode;
    return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
  }
}

function genNode (node, state) {
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(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
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
  1. 代码中elplain属性是在编译时发现的,如果节点没有属性,就会把plain设置为true
  2. 代码中主要逻辑是用genDatagenChildren分别获取datachildren,然后将它们分别拼到字符串中指定位置,最后把拼好的_c(tagName, data, children)返回
  3. data也是字符串,先给data赋值一个{,然后发现节点存在哪些属性数据,将这些数据拼接到data中,最后拼接一个}
  4. 生成children字符串逻辑也是拼接字符串。通过循环子节点列表,根据不同子节点类型生成不同节点字符串并将其拼接到一起。
  • 生成文本节点:把文本放在_v函数的参数中即可
function genText (text) {
  return ("_v(" + (text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}
1
2
3
4
5
  1. 判断文本类型:如果动态文本则使用expression;如果静态则使用text
  2. text使用JSON.stringfy原因是:可以给文本包装一层字符串
JSON.stringify('Hello') // "'Hello'"
1
  • 注释节点:只需要把文本放在_e参数中即可
function genComment (comment) {
  return ("_e(" + (JSON.stringify(comment.text)) + ")")
}
1
2
3

# 总结

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

  • 模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render
    • parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
    • optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
    • generate阶段:将最终的AST转化为render函数字符串,将字符串拼在with中返回给调用者
function generate (
  ast,
  options
) {
  var state = new CodegenState(options);
  // fix #11483, Root level <script> tags should not be rendered.
  var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
  return {
    render: ("with(this){return " + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}
1
2
3
4
5
6
7
8
9
10
11
12