> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/yuan-ma-jie-xi/vue-yuan-ma-jie-xi/vue2-yuan-ma-jie-xi-xi-lie-mu-ban-bian-yi.md).

# Vue2源码解析系列-模板编译

## 模板编译

浏览器其实并不能理解vue的模板语法，vue实际上是先利用编译器（compiler）在编译阶段实现**模板编译**，即将模板语法转换成vnode，然后在通过patch操作修改DOM来达到更改视图的目的。

模板编译包含几个重要操作和中间产物，大致流程如下：

```tsx
template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render函数
```

三个重要的步骤：

* parse： 解析template生成token并构建模板AST。
* transform： 将模板AST转换成能够生成JavaScript渲染函数代码的AST（codegenNode）
* generate：根据前面生成的AST来生成渲染函数代码。

## 解析模板并构建AST的思路

### HTML解析规范

事实上，解析html并构造token是有规范的，见[HTML规范](https://html.spec.whatwg.org/multipage/parsing.html)。

\*\*vue使用有限状态自动机来解析模板。\*\*所谓“有限状态”，就是指有限个状态，而“自动机”意味着随着字符的输入，解析器会自动地在不同状态间迁移。

例如上面的Data 状态，它规定了在当前状态下，不同的下个输入字符所能进入的不同状态以及相应的处理方法。像这样的状态有很多，解析过程就在这些状态中不停的流转，直到到达结尾。在这个过程中会创建很多token，利用这些token可以构建ast。

另外除了state外，HTML规范还定义了模式(mode)，规则如下：

* **Data state**。解析的初始模式。
* `<title>` 标签、`<textarea>` 标签，当解析器遇到这两个标签时，会切换到 **RCDATA** 模式；
* `<style>`、`<xmp>`、`<iframe>`、`<noembed>`、`<noframes>`、`<noscript>` 等标签，当解析器遇到这些标签时，会切换到 **RAWTEXT** 模式；
* 当解析器遇到 `<![CDATA[` 字符串时，会进入 **CDATA** 模式。

其他模式与Data模式的解析方式相似，简单起见，这里只考虑Data state。

以一个简单的html来说明解析流程：

```html
<h1>Vue</h1>
```

解析过程是这样：

1. 初始是Data state，读取下一个输入字符，发现是`<`，进入[tag open state](https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state)
2. 读取下一个输入字符，是ASCII码`h`，创建一个新的`start tag token`，将其标签名称设置为空字符串，切换到[tag name state](https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state)，并将当前字符作为下一个输入字符。
3. 读取下一个输入字符，是ASCII码`h`，将当前输入字符的小写版本添加到当前`tag token`的标签名称，不流转状态。
4. 读取下一个输入字符，是ASCII码`1`，将当前输入字符的小写版本添加到当前`tag token`的标签名称，不流转状态。
5. 读取下一个输入字符，发现是`>` ，发送当前`tag token`，进入[data state](https://html.spec.whatwg.org/multipage/parsing.html#data-state) 。
6. 读取下一个输入字符，发现是`V`，将该字符作为`character token` 发送 。
7. 读取下一个输入字符，发现是`u`，将该字符作为`character token` 发送 。
8. 读取下一个输入字符，发现是`e`，将该字符作为`character token` 发送 。
9. 读取下一个输入字符，发现是`<`，进入[tag open state](https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state)
10. 读取下一个输入字符，发现是`/` ，进入[end tag open state](https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state)
11. 读取下一个输入字符，发现是ASCII码`h`，创建一个新的`end tag token`，将其标签名称设置为空字符串，切换到[tag name state](https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state)，并将当前字符作为下一个输入字符。
12. 读取下一个输入字符，发现是ASCII码`h`，将当前输入字符的小写版本添加到当前`tag token`的标签名称，不流转状态。
13. 读取下一个输入字符，发现是ASCII码`1`，将当前输入字符的小写版本添加到当前`tag token`的标签名称，不流转状态。
14. 读取下一个输入字符，发现是`>` ，发送当前`tag token`，进入[data state](https://html.spec.whatwg.org/multipage/parsing.html#data-state) 。
15. 读取下一个输入字符串，EOF，发现读取完毕，发送`end-of-file token`。

在上面的例子中我们可以看到每次都只取出一个字符进行判断，并且每次判断完后都会自动读取下一个字符再判断(某些情况会将当前字符重新作为下一个字符)，假设当前组件模板字符串存于`context.source`中，我们可以通过不断的删除前面已经判断过的字符来使每次都能通过`context.source[0]`获取到下一个字符。

```tsx
while(context.source){
	if(mode=='data' && context.source[0] === '<'){
		mode = 'tag open'
		// ...
	}else if(mode == 'tag open' && /[a-z]/.test(context.source[0])){
		mode = 'tag name'
		// ...
	}else if(context.source[0] === '>'){
	  mode = 'data'
		// ...
	}
	// ...
	context.source = context.source.slice(1)
}
```

### 判断是否是自闭和标签

HTML中有些标签是自闭和的，例如`<br />` 。假如该标签是自闭和标签，则表示它没有闭合标签，解析方式与闭和标签不同，因此我们在解析标签时还得判断其是否是自闭和标签。

判断的方式有两种，一种是设立自闭和标签名单，然后判断标签名是否在自闭和名单之中。

另一种更简单的方法是判断标签是否以`/>`结束。

```tsx
let isSelfClosing = false
isSelfClosing = startsWith(context.source, '/>')
```

### 解析插值语法和attribute绑定

通过HTML规范我们已经能够实现解析HTML，但是vue的template与HTML并不完全相同，vue template对HTML做了增强，例如插值语法(`{{}}`)、Attribute 绑定等。

判断是否是插值语法很简单，只需要在文本状态时读取是否有`{{`即可，当读到`}}`表示插值语法结束。

而attribute属性也有规律可循，首先attribute属性是在标签内部，其次是`name=value`的形式，例如`<a href=”http://123.com” >` ，因此我们可以用正则来获取name和value，判断是否是attribute绑定也很简单，直接判断是否以`:` 、 `v-on`等开始即可。

```tsx
// Name.
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
// 假设此时context.source = ':href="">'，可以匹配到:href
const name = match[0]
// 删除已匹配的name和=
context.source = context.source.slice(name.length + 1)

if (!context.inVPre && /^(v-|:|\.|@|#)/.test(name)) {
	// 如果attribute有使用绑定，则需要进一步处理
}
```

value的处理分三种情况：被单引号包裹、被双引号包裹、没有引号包裹，例如`a='a'` 、 `b="b"` 和`c=c` 。被引号包裹的两种情况容易处理，第三种情况复杂一些，因为属性之间由空格、制表符等符号分隔，所以可以通过正则表达式`/^[^\t\r\n\f >]+/`匹配到从当前字符到下个属性或标签结束标志(>号)前的内容，例如`foo=value a=c>` ，因为name和=被使用过删除了，因此剩下`value a=c>`，正则表达式`/^[^\t\r\n\f >]+/`能匹配到`value。`

这里还要注意，如果是不被引号包裹，那么`value`就不能再出现引号等特殊符号。例如`foo=f"oot"`，这可能是用户书写错误，可以用正则表达式`/["'<=`]/g\`提取并发送错误。

```tsx
// a='a' b="b" c=c 
const quote = context.source[0]
const isQuoted = quote === `"` || quote === `'`
if (isQuoted) {
	// value被引号包裹的情况
	const endIndex = context.source.indexOf(quote)
	context.source = context.source.slice(1) // 删除左侧的引号
	if (endIndex === -1) {
		// 如果没有右侧的引号，则将source后面的所有内容都作为attribute
    content = parseTextData(
      context,
      context.source.length,
      TextModes.ATTRIBUTE_VALUE
    )
  } else {
		// 如果有右侧的引号，则提取引号内的value
    content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
  }
}
else{
	// 没有引号包裹的情况
	const match = /^[^\t\r\n\f >]+/.exec(context.source);
	if (!match) {
	    return undefined;
	}
	const unexpectedChars = /["'<=`]/g;
  let m;
  while ((m = unexpectedChars.exec(match[0]))) {
			// 将引号的位置发送给错误处理机制，提醒用户。
      emitError(
        context,
        ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
        m.index
      )
  }
	content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
}
```

属性解析阶段结束的标志应当是下一个输入字符为`>`或者`/>`的时候。所以整个属性解析过程应该是在while循环的内部。

```tsx
while (
  context.source.length > 0 &&
  !startsWith(context.source, '>') &&
  !startsWith(context.source, '/>')
) {
	const attr = parseAttribute(context, attributeNames)
	 // ...
}
```

### 构建AST

通过上一节的介绍，我们已经能够解析template，下一步就是根据解析过程中生成的token来构建AST。

以下面这个例子：

```html
<h1><span>Vue</span></h1>
```

通过解析获得了下面的tokens：

```tsx
[
{type: "start tag", tagName: "h1"},
{type: "start tag", tagName: "span"},
{type: "text", value: "Vue"},
{type: "end tag", tagName: "span"},
{type: "end tag", tagName: "h1"},
{type: "end-of-file"}
]
```

ast是树结构，我们需要扫描tokens来构建这颗树，这里需要借助栈结构。每次从tokens出队一个节点，如果type是开始标签，则作为栈顶的子节点，并将其添加进栈结构中，作为下一个节点的父节点，如果type是结束标签，则移除栈顶节点，当type为EOF时结束。

```tsx
const tokens = [
	{ type: "start tag", tagName: "h1" },
	{ type: "start tag", tagName: "span" },
	{ type: "text", value: "Vue" },
	{ type: "end tag", tagName: "span" },
  { type: "end tag", tagName: "h1" },
  {type: "end-of-file"}
];
const root = {type: "root",children: []}
const nodeStack = [root];
while (nodeStack.length) {
    const parent = nodeStack[nodeStack.length-1];
    const currentNode = tokens.shift();
    if (currentNode.type === 'start tag') {
        const element = {type: 'element', tag: currentNode.tagName, children:[]}
        parent.children.push(element);
        nodeStack.push(element)
    } else if (currentNode.type === 'text') {
        const textNode = {type: 'text', value: currentNode.value}
        parent.children.push(textNode)
    } else if (currentNode.type === 'end tag') {
        nodeStack.pop()
    } else if(currentNode.type === 'end-of-file'){
        break;
    }
}

console.log(root);
```

## vue-compiler源码

在实际的vue源码中，由compiler来完成从解析模板到生成渲染函数的整个过程。

在vue的源码中有4个负责compiler的package，分别是：

* `compiler-core`
* `compiler-dom`
* `compiler-sfc`
* `compiler-ssr`

`compiler-core`存放compile核心逻辑代码，这部分代码无关运行环境，`compiler-sfc` 提供转换sfc为渲染函数的底层API，而`compiler-ssr`和`compiler-dom`则是分别提供SSR环境下和DOM环境下的编译API。

> compiler-ssr这部分不展开讲

vue编译器最主要的作用就是vue模板转换成渲染函数，然后再由vue渲染器渲染成DOM。

vue编译器的主要步骤是：

**parse函数将vue单页面组件转换成模板ast，然后再调用transform函数将模板ast进行转换，最后调用generator函数将ast转换成渲染函数。**

### parse

> vue解析模板最核心的代码存放在@vue/compiler-core下的parse.ts文件中。

vue解析模板的入口是`baseParse`函数。

```tsx
export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}
```

其中最重要的解析逻辑位于`parseChildren`函数中，其实`parseChildren`函数就相当于一个状态机了。

实际上vue源码要比我们前面写的代码复杂一些，因为vue考虑的情况更多，node的种类也比较多。下面是解析element和text生成的token。

```tsx
// 解析标签
function parseTag(
  context: ParserContext,
  type: TagType,
  parent: ElementNode | undefined
): ElementNode | undefined{
// ...
	return {
    type: NodeTypes.ELEMENT, // 节点类型，例如root、element、text、comment等
    ns, // Namespace
    tag, // 标签名
    tagType, // element、component、slot、template
    props,
    isSelfClosing,  // 是否是自闭和标签
    children: [],
    loc: getSelection(context, start),
    codegenNode: undefined // to be created during transform phase
  }
}
// 解析文本
function parseText(context: ParserContext, mode: TextModes): TextNode {
	// ...
	return {
    type: NodeTypes.TEXT,
    content,
    loc: getSelection(context, start)
  }
}
```

vue定义了很多函数来表示html解析状态，例如：

* parseChildren： 代表`data state`。
* parseElement：包含`tag open state` 和`tag end state` 两个阶段。
* parseTag： 代表`tag name state` 。
* parseText： 解析文本。

按照HTML规范，每当进入另一个状态时，在代码的体现就是调用对应的函数。例如从`data state`到`tag open state`，实际上就是在`parseChildren`中调用`parseElement`函数。

```tsx
// 注意，为了方便理解，省略了部分不重要的代码
function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
	// ancestors是一个栈，调用last函数获取这个栈的栈顶元素
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = []

  while (!isEnd(context, mode, ancestors)) {

    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // 插值语法， 即匹配'{{'
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        // Tag open state
        if (s.length === 1) {
          emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
        } else if (s[1] === '!') {
          // markup declaration open state.
          if (startsWith(s, '<!--')) {
						// 注释节点
            node = parseComment(context)
          } else if (startsWith(s, '<!DOCTYPE')) {
            // 忽略文档声明.
            node = parseBogusComment(context)
          }  else {
            emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
            node = parseBogusComment(context)
          }
        }  else if (/[a-z]/i.test(s[1])) {
					// tag open state
          node = parseElement(context, ancestors)
        } else {
          emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
        }
      }
    }
    if (!node) {
			// 解析文本
      node = parseText(context, mode)
    }

    if (isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(nodes, node[i])
      }
    } else {
      pushNode(nodes, node)
    }
  }
	// ...
  return nodes
}
```

当出现不合规范的模板语法时，vue会通过`emitError`来发送`error` 提示用户，**context对象包含当前解析字符的信息，包含source(模板字符串)、line(当前字符第几行)、column(当前字符第几列)、offset(当前字符串的偏移量)等**，vue的错误处理机制会通过这些信息将错误代码复现给用户，以便快速找到错误。

ancestors事实上就相当于我们前面所说的`nodeStack`栈，通过这个栈来构建ast树结构，`last(ancestors)`实际上就是获取这个栈的栈顶元素，这个栈顶元素就是当前节点的父节点。

**vue不会先扫描完source生成所有的token后再构建ast，而是边扫描source生成token边构建ast。**

### transform

通过parse操作我们已经成功地将模板语法转换成了AST（抽象语法树），我们的最终目的是要将其转换成渲染函数代码，因此需要将前面的模板AST进行转换，以便后面更好地生成渲染函数代码。我们设计一个`transform`函数来实现。

> `transform`函数位于@vue/compiler-core包下transform.ts文件中。

前面我们讲过，ast是一个N叉树，那么我们要将其转换就应该对它进行深度优先遍历。我们使用`traverseNode`函数实现深度遍历逻辑，为了方便，我们将循环遍历`node.children`的操作放到`traverseChildren`函数里。

```tsx
function traverseNode(node, context) {
	context.currentNode = node;
    context.childrenIndex = 0;
    
    /* 处理逻辑 */ 
    if (node.type === 'element' && node.props.find(i=>i.name==='if')) {
        // ...
    }
    if (node.type === 'element' && node.props.find(i=>i.name==='slot')) {
        // ...
    }
		// 这里省略一大段代码
    // if ...

  // 深度优先遍历
	if (node.type === "element" || node.type === "root") {
		traverseChildren(node.context);
    } else if (node.type === "comment") {
        // ...
    }
    
    context.currentNode = node
}
function traverseChildren(parent, context) {
	for (let i = 0; i < parent.children.length; i++) {
		const child = parent.children[i];
		context.parent = parent;
		context.childIndex = i;
		traverseNode(child, context);
	}
}

const nodes = [
	{
		type: "root",
		children: [
			{
				type: "element",
				tag: "div",
				children: [
					{
						type: "element",
                        tag: 'h1',
                        props: [{
                            name: "foo",
                            value: "value"
                        }],
						children: [
							{
								type: "text",
								value: "Hello World",
							},
						],
					},
				],
			},
			{
				type: "element",
				tag: "br",
				children: [],
			},
		],
	},
];

traverseNode(nodes, context)
```

随着转换逻辑越来越多，越来越复杂，我们的`traverseNode` 函数也会越来越臃肿，因此我们可以将这些转换逻辑提取到一个个函数中去。更好的办法是以插件化的形式来实现，也就是将这些转换函数当成一个个插件放到一个插件数组中去，然后循环调用这个插件数组中的每一个插件函数，这个插件数组就是`context.nodeTransforms`。

在执行这些插件函数时会将上下文`context`对象一同传入，我们可以提前将一些信息挂载到这个上下文对象中，例如将当前处理的`node`对象挂载到`context.currentNode`中，之后在插件函数执行时就可以通过这个`context`对象来获取一些我们想要的信息，例如当前节点类型`context.currentNode.type` 。

```tsx
function traverseNode(node, context) {
	context.currentNode = node;
	context.childrenIndex = 0;

	/* 处理逻辑 */
	const { nodeTransforms } = context;
	for (let i = 0; i < nodeTransforms.length; i++) {
		nodeTransforms[i](node);
	}
	// ...
}

traverseNode(nodes, {
	nodeTransforms: [
		transformOnce,
		transformIf,
		transformMemo,
		transformFor,
		...(__COMPAT__ ? [transformFilter] : []),
		...(!__BROWSER__ && prefixIdentifiers
			? [
					// order is important
					trackVForSlotScopes,
					transformExpression,
			  ]
			: __BROWSER__ && __DEV__
			? [transformExpression]
			: []),
		transformSlotOutlet,
		transformElement,
		trackSlotScopes,
		transformText,
	],
});
```

### generate

现在就只差最后一步了，那就是生成渲染函数代码，这部分由`generate`函数完成。

> generate的逻辑存放在@vue/compiler-core包下的codegen.ts文件中。

#### codegenNode

`transform`后的`ast`存放在`ast.codegenNode`属性中。例如下面这段代码，经过parse和transform后会变成这样：

```tsx
<div>
	<h1 :foo="num">Hello World</h1>
</div>
```

```tsx
codegenNode = {
  type: 13,
  tag: "div",
  props: undefined,
  children: [
    {
      type: 1,
      ns: 0,
      tag: "h1",
      tagType: 0,
      props: [
        {
          type: 7,
          name: "bind",
          exp: {
            type: 4,
            content: "_ctx.num",
            isStatic: false,
            constType: 0,
            loc: {
              // ...
              source: "num",
            },
          },
          arg: {
            type: 4,
            content: "foo",
            isStatic: true,
            constType: 3,
            loc: {
              // ...
              source: "foo",
            },
          },
          modifiers: [
          ],
          loc: {
            // ...
            source: ":foo=\"num\"",
          },
        },
      ],
      isSelfClosing: false,
      children: [
        {
          type: 2,
          content: "Hello World",
          loc: {
            // ...
            source: "Hello World",
          },
        },
      ],
      loc: {
        // ...
        source: "<h1 :foo=\"num\">Hello World</h1>",
      },
      codegenNode: {
        type: 13,
        tag: "\"h1\"",
        props: {
          type: 15,
          loc: {
            // ...
            source: "<h1 :foo=\"num\">Hello World</h1>",
          },
          properties: [
            {
              type: 16,
              loc: {
                source: "",
                // ...
              },
              key: {
                type: 4,
                content: "foo",
                isStatic: true,
                constType: 3,
                loc: {
                  // ...
                  source: "foo",
                },
              },
              value: {
                type: 4,
                content: "_ctx.num",
                isStatic: false,
                constType: 0,
                loc: {
                  // ...
                  source: "num",
                },
              },
            },
          ],
        },
        children: {
          type: 2,
          content: "Hello World",
          loc: {
            // ...
            source: "Hello World",
          },
        },
        patchFlag: "8 /* PROPS */",
        dynamicProps: {
          type: 4,
          loc: {
            source: "",
            // ...
          },
          content: "_hoisted_1",
          isStatic: false,
          constType: 2,
          hoisted: {
            type: 4,
            loc: {
              source: "",
              // ...
            },
            content: "[\"foo\"]",
            isStatic: false,
            constType: 0,
          },
        },
        directives: undefined,
        isBlock: false,
        disableTracking: false,
        isComponent: false,
        loc: {
          // ...
          source: "<h1 :foo=\"num\">Hello World</h1>",
        },
      },
    },
  ],
  patchFlag: undefined,
  dynamicProps: undefined,
  directives: undefined,
  isBlock: true,
  disableTracking: false,
  isComponent: false,
  loc: {
    // ...
    source: "<div>\r\n\t\t<h1 :foo=\"num\">Hello World</h1>\r\n\t</div>",
  },
}
```

`codegenNode`存放着能够生成渲染函数的一切信息，我们可以通过遍历`codegenNode`来生成渲染函数代码。

在经过generate后会生成最终的渲染函数代码：

```tsx
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", { foo: _ctx.num }, "Hello World", 8 /* PROPS */, ["foo"])
  ]))
}
```

> 可以在vue模板编译预览网站查看[Vue Template Explorer (vuejs.org)](https://template-explorer.vuejs.org/#eyJzcmMiOiJcdDxkaXY+XG5cdFx0PGgxIDpmb289XCJudW1cIj5IZWxsbyBXb3JsZDwvaDE+XG5cdDwvZGl2PiIsIm9wdGlvbnMiOnt9fQ==)模板编译结果，在控制台可以看到AST。

首先渲染函数以函数声明开头，因此我们可以直接写出：

```tsx
const functionName = ssr ? `ssrRender` : `render`;
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache'];
if (options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options');
}
const signature = options.isTS
    ? args.map(arg => `${arg}: any`).join(',')
    : args.join(', ');

push(`function ${functionName}(${signature}) {`);
indent()
push(`return `)
genNode(ast.codegenNode, context)
deindent()
push(`}`)

function push(code){
	context.code += code; // 将代码添加到context.code中
  // 。。。省略一些处理
}
```

`functionName`是渲染函数的函数名，`signature` 则是函数形参。`push`函数主要的作用是将code代码添加到`context.code`中，`indent` 和`deindent`函数则是为生成的`context.code`增加/删除缩进。

#### genNode()

**`genNode`函数则是生成渲染函数内部逻辑的方法。**

```tsx
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  if (isString(node)) {
    context.push(node)
    return
  }
  if (isSymbol(node)) {
    context.push(context.helper(node))
    return
  }
  switch (node.type) {
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      __DEV__ &&
        assert(
          node.codegenNode != null,
          `Codegen node is missing for element/if/for node. ` +
            `Apply appropriate transforms first.`
        )
      genNode(node.codegenNode!, context)
      break
    case NodeTypes.TEXT:
      genText(node, context)
      break
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break
    case NodeTypes.TEXT_CALL:
      genNode(node.codegenNode, context)
      break
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context)
      break
    case NodeTypes.COMMENT:
      genComment(node, context)
      break
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context)
      break
    // ...
    default:
      if (__DEV__) {
        assert(false, `unhandled codegen node type: ${(node as any).type}`)
        // make sure we exhaust all possible types
        const exhaustiveCheck: never = node
        return exhaustiveCheck
      }
  }
}
```

`codegenNode` 本质上是一颗N叉树，因此vue采用递归的方式进行深度遍历。例如在创建元素vnode时(即生成\_createElementVNode函数时)，`genNode`会调用`genVNodeCall`函数(此时`node.type===NodeTypes.VNODE_CALL`)，`genVNodeCall`函数内部会调用`genNodeList`函数，而`genNodeList`函数又会遍历`node.children`并将其作为参数传入`genNode`函数中。

```tsx
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure } = context
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock,
    disableTracking,
    isComponent
  } = node
	// ...
  genNodeList(
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  )
  push(`)`)
  // ...
}

function genNodeList(
  nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
  context: CodegenContext,
  multilines: boolean = false,
  comma: boolean = true
) {
  const { push, newline } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else if (isArray(node)) {
      genNodeListAsArray(node, context)
    } else {
      genNode(node, context)
    }
    if (i < nodes.length - 1) {
      if (multilines) {
        comma && push(',')
        newline()
      } else {
        comma && push(', ')
      }
    }
  }
}
```

#### helper()

vue渲染函数内部会调用不同的函数来创造不同类型的vnode，而这些函数名都保存在`helperNameMap`中。

```tsx
export const helperNameMap: any = {
  [FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  [KEEP_ALIVE]: `KeepAlive`,
  [BASE_TRANSITION]: `BaseTransition`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_ELEMENT_VNODE]: `createElementVNode`,
  [CREATE_COMMENT]: `createCommentVNode`,
  [CREATE_TEXT]: `createTextVNode`,
  [CREATE_STATIC]: `createStaticVNode`,
  [RESOLVE_COMPONENT]: `resolveComponent`,
  [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
  [RESOLVE_DIRECTIVE]: `resolveDirective`,
  [RESOLVE_FILTER]: `resolveFilter`,
  [WITH_DIRECTIVES]: `withDirectives`,
  [RENDER_LIST]: `renderList`,
  [RENDER_SLOT]: `renderSlot`,
  [CREATE_SLOTS]: `createSlots`,
  [TO_DISPLAY_STRING]: `toDisplayString`,
  [MERGE_PROPS]: `mergeProps`,
  [NORMALIZE_CLASS]: `normalizeClass`,
  [NORMALIZE_STYLE]: `normalizeStyle`,
  [NORMALIZE_PROPS]: `normalizeProps`,
  [GUARD_REACTIVE_PROPS]: `guardReactiveProps`,
  [TO_HANDLERS]: `toHandlers`,
  [CAMELIZE]: `camelize`,
  [CAPITALIZE]: `capitalize`,
  [TO_HANDLER_KEY]: `toHandlerKey`,
  [SET_BLOCK_TRACKING]: `setBlockTracking`,
  [PUSH_SCOPE_ID]: `pushScopeId`,
  [POP_SCOPE_ID]: `popScopeId`,
  [WITH_SCOPE_ID]: `withScopeId`,
  [WITH_CTX]: `withCtx`,
  [UNREF]: `unref`,
  [IS_REF]: `isRef`,
  [WITH_MEMO]: `withMemo`,
  [IS_MEMO_SAME]: `isMemoSame`
}
```

vue通过调用helper函数来获取这些函数名，例如`_createCommentVNode` ：

```tsx
function helper(key) {
  return `_${helperNameMap[key]}`;
}

function genComment(node: CommentNode, context: CodegenContext) {
  const { push, helper, pure } = context
  if (pure) {
    push(PURE_ANNOTATION)
  }
  push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
}
```

#### Block

渲染函数和我们平常使用的`h`函数它们的目的都是为了创建`vnode`。但是经过模板编译生成的渲染函数比`h`函数多了一个令人困惑的`Block` ：

```
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", { foo: _ctx.num }, "Hello World", 8 /* PROPS */, ["foo"])
  ]))
}
```

关于这个Block的作用，在vue官网由提及： [渲染机制 | Vue.js (vuejs.org)](https://cn.vuejs.org/guide/extras/rendering-mechanism.html#tree-flattening)

> **这里我们引入一个概念“区块”(Block)，内部结构是稳定的一个部分可被称之为一个区块。在这个用例中，整个模板只有一个区块，因为这里没有用到任何结构性指令 (比如 `v-if`或者 `v-for`)。**

\*\*每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点)。\*\*所谓的更新标记是指`patchFlag` ，例如上面代码中`patchFlag`就是`8` 。patchFlag的作用是为了提升虚拟 DOM 运行时性能，一个元素可以有多个更新类型标记，它们会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记，确定相应的更新操作。

假如这里使用v-if等结构性指令，那么模板编译后的渲染函数是这样的：

```tsx
<div>
	<h1 v-if="isShow">Hello World</h1>
</div>

// 编译后的代码
import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    (_ctx.isShow)
      ? (_openBlock(), _createElementBlock("h1", { key: 0 }, "Hello World"))
      : _createCommentVNode("v-if", true)
  ]))
}
```

openBlock必须要在“createBlock”前调用，它会初始化`currentBlock`，并将该`currentBlock`加入到`blockStack`中。由于Block树是能够嵌套的，因此用一个栈结构来存放所有的Block树。

````tsx
// Since v-if and v-for are the two possible ways node structure can dynamically
// change, once we consider v-if branches and each v-for fragment a block, we
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
export const blockStack: (VNode[] | null)[] = []
export let currentBlock: VNode[] | null = null

/**
 * Open a block.
 * This must be called before `createBlock`. It cannot be part of `createBlock`
 * because the children of the block are evaluated before `createBlock` itself
 * is called. The generated code typically looks like this:
 *
 * ```js
 * function render() {
 *   return (openBlock(),createBlock('div', null, [...]))
 * }
 * ```
 * disableTracking is true when creating a v-for fragment block, since a v-for
 * fragment always diffs its children.
 *
 * @private
 */
export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}
export function closeBlock() {
  blockStack.pop()
  currentBlock = blockStack[blockStack.length - 1] || null
}
````

后面的`createElementBlock`函数内部会先调用`createBaseVNode`函数创建`vnode`，然后再调用`setupBlock`函数。此外渲染函数中用于创建元素vnode的`createElementVNode`函数其实就是`createBaseVNode` 函数的别名。

```tsx
// createElementVNode函数其实就是createBaseVNode函数的别名
export { createBaseVNode as createElementVNode }

export function createElementBlock(
  type: string,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[],
  shapeFlag?: number
) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */
    )
  )
}

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
	// ...

  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }
  return vnode
}

function setupBlock(vnode: VNode) {
  // save current block children on the block vnode
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  // close block
  closeBlock()
  // a block is always going to be patched, so track it as a child of its
  // parent block
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}
```

可以看到`currentBlock`在追踪当前Block树的所有\*\*带更新类型标记的后代节点。\*\*最后在`setupBlock`函数中将所有带更新类型标记的后代节点挂载到Block节点的`dynamicChildren`属性下，这与官网的对Block的描述相对应。

## 总结

Vue模板编译的目的是将模板转换成渲染函数，期间会经过解析（parse）、转换（transform）和生成（generate）等操作，最终形成浏览器能够识别的JavaScript代码。

```tsx
template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render
```

Vue模板编译主要依靠vue-compiler完成，将模板转换成渲染函数的目的是生成vnode，以便后续的vue-runtime能够动态更改视图（DOM）。
