# 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）。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET 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?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
