Vue源码学习(贰)

200 阅读10分钟

vue.js 核心模块(下)

模板编译

<template>
	<div @click="handleClick">{{ count }}</div>
	<span></span>
</template>

渲染流程

2025-06-19-20-03-08-image.png

模板解析流程

  1. 模板解析阶段主要做的工作是把用户在<template></template>标签内写的模板使用正则等方式解析成抽象语法树AST。这一阶段在源码中对应解析器(parser)模块

    • 解析器:vue\src\compiler\parser\index.js,就是把用户写的模板根据一定的解析规则解析出有效信息,最后用这些信心形成AST

    • 在模板内,除了有常规的 HTML 标签外,用户还会一些文本信息以及在文本信息中包含过滤器。而这些不同的内容在解析起来肯定需要不同的解析规则,所以解析器不可能只有一个,它应该除了有解析常规 HTML 的 HTML 解析器,还应该有解析文本的文本解析器以及解析文本中如果包含过滤器的过滤器解析器。

    • 文本信息和标签属性信息却又是存在于 HTML 标签之内的,所以在解析整个模板的时候它的流程应该是这样子的:HTML 解析器是主线,先用 HTML 解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。如下图所示:

2025-06-19-20-12-09-image.png

        export const createCompiler = createCompilerCreator(function baseCompile(
        	template: string,
        	options: CompilerOptions
        ): CompiledResult {
        	// parse 会用正则等方式解析 template 模板中的指令、class,style等数据,并生成 AST
        	const ast = parse(template.trim(), options);
        	// optimize 作用是标记静态节点
        	// 在 patch 过程中,DOM-DIFF 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能
        	if (options.optimize !== false) {
        		optimize(ast, options);
        	}
        	// 把 AST 转换成 render 函数字符串的过程,
        	// 得到的是 render 函数字符串以及 staticRenderFns 数组
        	const code = generate(ast, options);
        	return {
        		ast,
        		render: code.render,
        		staticRenderFns: code.staticRenderFns,
        	};
        });
        
  • 静态节点判断
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))
	);
}
  • 标记静态节点
// 遍历整个AST树,并检查每个节点是否是静态的。如果节点是静态的,则会设置其static属性为true。
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;
				}
			}
		}
	}
}

type 类型

  • type 为 1 : 表示元素节点
  • type 为 2 : 表示包含变量的动态文本节点
  • type 为 3 : 表示不包含变量的纯文本节点 判断静态节点后,需要给静态节点添加标记
// 标记模板中的静态根节点。遍历整个AST树,并检查每个节点是否是静态根节点。如果节点是静态根节点,则会设置其staticRoot属性为true。
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);
			}
		}
	}
}

HTML 解析器

vue 项目中代码地址 vue\src\compiler\parser\index.js line:79

parseHTML(){
    // 解析到开始标签时,调用这个函数,生成元素节点的AST
    start(tag, attrs, unary){
        let element: ASTElement = createASTElement(tag, attrs, currentParent);
    }

        // 解析到结束标签时,调用 end 函数
    end(tag,start,end){
        closeElement(element)
    }
    // 解析到文本时,调用 chats 函数生成文本类型的AST
        chars (text: string, start: number, end: number) {
        // 带变量的动态文本
                if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          }
        }
    }
        // 解析到注释时,调用 comment 函数
        comment (text: string, start, end) {
      // adding anyting as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
}
  • 文本

  • HTML 标签

    • 注释 <!-- -->
    • 条件注释 <!-- [if !!IE] -->
    // 匹配注释的开始、结束
    const comment = /^<!\--/;
    // Comment:
    if (comment.test(html)) {
        const commentEnd = html.indexOf('-->')
    
        if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
                options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            // 移动解析游标,index的值从0 移动到最后,不包括标签的尖括号
            /**
             * index 0
             * advance(3)
             * index 3 (v)
             * <div >{{ greeting }} World!</div>
             *  */
            advance(commentEnd + 3)
            continue
        }
    }
    

如何保证 AST 节点的层级关系

Vue 在 HTML 解析器的开头定义了一个栈 stack,用来存储 AST 节点,这个栈的作用是维护 AST 节点的层级关系。 e.g.:<div> <p> <span></span> </p></div>

stack.png

  • 这个栈也可以检测模版字符串中是否有未正确闭合的标签
<div><p><span></p></div>

文本解析器

HTML解析器解析到文本内容时会调用 4 个钩子函数中的chars函数来创建文本型的AST节点,并且说了在chars函数中会根据文本内容是否包含变量再细分为创建含有变量的AST节点和不含有变量的AST节点

const text = data ? data : "xxx";
// 当解析到标签的文本时,触发 chars 函数
chars(text: string, start: number, end: number) {
    // 带变量的动态文本
    if ((res = parseText(text, delimiters))) {
        let element = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text,
        };
    } else {
        let element = {
            type: 3,
            text,
        };
    }
}
  • 上面代码:创建含有变量的 AST 节点时节点的 type 属性为 2,并且相较于不包含变量的 AST 节点多了两个属性: expression 和 tokens 。那么如何来判断文本里面是否包含变量以及多的那两个属性是什么呢?这就涉及到文本解析器了,当 Vue 用 HTML 解析器解析出文本时,再将解析出来的文本内容传给文本解析器,最后由文本解析器解析该段文本里面是否包含变量以及如果包含变量时再解析 expression 和 tokens 。

假设通过 HTML 解析器得到这样的文本:

let text = "我叫{{ name }},今年{{ age }}岁";
let res = parseText(text);
res = {
	expression: "我叫" + _s(name) + ",今年" + _s(age) + "岁",
	tokens: ["我叫", { "@binding": name }, ", 今年", { "@binding": age }, "岁"],
};
/* @flow */

import { cached } from "shared/util";
import { parseFilters } from "./filter-parser";

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;

const buildRegex = cached((delimiters) => {
	const open = delimiters[0].replace(regexEscapeRE, "\\$&");
	const close = delimiters[1].replace(regexEscapeRE, "\\$&");
	return new RegExp(open + "((?:.|\\n)+?)" + close, "g");
});

type TextParseResult = {
	expression: string,
	tokens: Array<string | { "@binding": string }>,
};

export function parseText(
	text: string,
	delimiters?: [string, string]
): TextParseResult | void {
	// 检查文本是否包含变量, 变量的格式为 {{xxx}},检测文本中是否包含双大括号。
	// 不传入参数时默认为 defaultTagRE 双大括号正则匹配
	const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
	if (!tagRE.test(text)) {
		return;
	}
	const tokens = [];
	const rawTokens = [];
	let lastIndex = (tagRE.lastIndex = 0);
	let match, index, tokenValue;

	// 循环检测文本中是否包含变量
	// tagRe.exec("hello, {{ name }}, I am {{ age }} years old") 返回:
	// ["{{ name }}", "name", index: 7, input: "hello, {{ name }}, I am {{ age }} years old"]
	while ((match = tagRE.exec(text))) {
		index = match.index;
		// push text token
		if (index > lastIndex) {
			// 表示变量前面有纯文本
			rawTokens.push((tokenValue = text.slice(lastIndex, index)));
			tokens.push(JSON.stringify(tokenValue));
		}
		// 表示文本一开始就是变量 {{ name }}
		// tag token
		const exp = parseFilters(match[1].trim());
		tokens.push(`_s(${exp})`);
		rawTokens.push({ "@binding": exp });
		// 更新 lastIndex,保证在下一轮循环的时候,只从 }} 之后的位置开始匹配
		lastIndex = index + match[0].length;
	}
	// 当剩下的 text 不再被正则匹配的时候,表示所有变量都已经处理完毕
	// 如果此时 lastIndex < text.length 表示在最后一个变量后还有剩余的文本,那么就把剩余的文本当做纯文本,加入到 tokens 中
	if (lastIndex < text.length) {
		rawTokens.push((tokenValue = text.slice(lastIndex)));
		tokens.push(JSON.stringify(tokenValue));
	}
	return {
		expression: tokens.join("+"),
		tokens: rawTokens,
	};
}

优化阶段

在模板编译的时候就先找出模板中所有的静态节点和静态根节点,然后给他们打上标记,用于告诉后面的patch过程,打了标记的这些节点是不需要对比的,这就是优化阶段

// {/* 静态根节点 - ul */}
// 静态节点 - li
<ul>
	<li>1</li>
	<li>2</li>
	<li>3</li>
</ul>
  1. AST中找出所有静态节点并打上标记
  2. AST中找出所有的根节点,并打上标记
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) {
			// 如果当前节点的子节点标签带有 v-if,v-else-if,v-for等指令,那么需要把当前节点标记为非静态节点
			// v-if 指令的 block ……
			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 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.
		// 静态根节点: 节点自身必须是静态节点,节点必须要有子节点 children ,子节点不能是只有一个纯文本节点
		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);
			}
		}
	}
}

代码生成阶段

所谓代码生成阶段其实就是根据模板对应的抽象语法树AST生成一个函数,通过调用这个函数就可以得到模板对应的虚拟DOM

  • 如何根据AST生成render函数
<div id="app">
	<span>hello {{ name }}</span>
</div>;

// AST 如下:
const astTree = [
	{
		type: 1,
		ns: 0,
		tag: "div",
		tagType: 0,
		props: [
			{
				type: 6,
				name: "id",
				value: {
					type: 2,
					content: "app",
				},
			},
		],
		isSelfClosing: false,
		children: [
			{
				type: 1,
				ns: 0,
				tag: "span",
				tagType: 0,
				props: [],
				isSelfClosing: false,
				children: [
					{
						type: 2,
						content: "hello ",
					},
					{
						type: 5,
						content: {
							type: 4,
							isStatic: false,
							isConstant: false,
							content: "name",
						},
					},
				],
			},
		],
	},
];
  1. 首先,根节点 div 是一个元素类型的 AST 节点,我们需要创建一个元素类型的 VNode
_c("div", { attrs: { id: "app" }, [/* 子节点列表 */] });
  1. 根节点 div 具有子节点,那我们进入子节点列表 children 遍历子节点,发现子节点 span 是一个元素类型的 AST 节点,我们需要创建一个元素类型的 VNode
_c("div", { attrs: { id: "app" },
    [ _c("span", {attrs:{}}, [ /* 子节点列表 */])]
    });
  1. 继续遍历 span 节点的子节点,发现是一个文本型节点,我们就创建一个文本型 VNode 并将其插入到 span 节点的子节点列表中
_c("div", { attrs: { id: "app" },
    [ _c("span", {attrs:{}}, [ _v("hello " + _s(name))])]
    });
  1. 遍历完之后,把得到的代码包装一下,如下:
with (this) {
	return _c(
		"div",
		{
			attrs: { id: "app" },
		},
		[_c("span", { attrs: {} }, [_v("hello " + _s(name))])]
	);
}
  1. 最后,将上面得到的函数字符串传递给 createFunction 函数, createFunction 函数会帮我们把得到的字符串转换为真正的函数,赋给组件中的 render 选型,从而就是 render 函数了
res.render = createFunction(coplied.render, fnGenErrors);

function createFunction(code, errors) {
	try {
		return new Function(code);
	} catch (err) {
		errors.push({ err, code });
		return noop;
	}
}
  • 代码生成阶段的源码文件位置:vue\src\compiler\codegen\index.js
// 生成函数
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,
	};
}

// 生成元素。 genElement 函数就是根据当前 AST 元素节点属性的不同,从而执行不同的代码生成函数
export function genElement(el: ASTElement, state: CodegenState): string {
	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
		let code;
		if (el.component) {
			code = genComponent(el.component, el, state);
		} else {
			let data;
			if (!el.plain || (el.pre && state.maybeComponent(el))) {
				data = 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;
	}
}
  1. 生成元素节点
function genNormalElement(el, state, stringifyChildren) {
	const data = el.plain ? undefined : genData(el, state);
	const children = stringifyChildren
		? `[${genChildrenAsStringNode(el, state)}]`
		: genSSRChildren(el, state, true);
	return `_c('${el.tag}'${data ? `,${data}` : ""}${
		children ? `,${children}` : ""
	})`;
}

// genData 就是在拼接字符串,先给 data 赋值一个 { , 然后再判断存在哪些属性数据,就把对应的数据拼接到 data 中,最后再拼接 },得到全部属性 data
export function genData(el: ASTElement, state: CodegenState): string {
	let data = "{";

	// directives first.
	// directives may mutate the el's other properties before they are generated.
	const 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 (let 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) {
		const 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;
}
  1. 获取子节点列表 children
export function genChildren(
	el: ASTElement,
	state: CodegenState,
	checkSkip?: boolean,
	altGenElement?: Function,
	altGenNode?: Function
): string | void {
	const children = el.children;
	if (children.length) {
		const el: any = children[0];
		// optimize single v-for
		if (
			children.length === 1 &&
			el.for &&
			el.tag !== "template" &&
			el.tag !== "slot"
		) {
			const normalizationType = checkSkip
				? state.maybeComponent(el)
					? `,1`
					: `,0`
				: ``;
			return `${(altGenElement || genElement)(el, state)}${normalizationType}`;
		}
		const normalizationType = checkSkip
			? getNormalizationType(children, state.maybeComponent)
			: 0;
		const gen = altGenNode || genNode;
		return `[${children.map((c) => gen(c, state)).join(",")}]${
			normalizationType ? `,${normalizationType}` : ""
		}`;
	}
}
  1. 获取节点类型
function genNode(node: ASTNode, state: CodegenState): string {
	if (node.type === 1) {
		return genElement(node, state);
	} else if (node.type === 3 && node.isComment) {
		return genComment(node);
	} else {
		return genText(node);
	}
}
  1. 文本节点

    • 文本型的调用 _v(text) 创建

      export function genText(text: ASTText | ASTExpression): string {
      	return `_v(${
      		text.type === 2
      			? text.expression // no need for () because already wrapped in _s()
      			: transformSpecialNewlines(JSON.stringify(text.text))
      	})`;
      }
      
  2. 注释节点

    • 注释型 VNode 调用 _e(text) 创建

      export function genComment(comment: ASTText): string {
      	return `_e(${JSON.stringify(comment.text)})`;
      }
      

总结

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,
				{
					outputSourceRange: process.env.NODE_ENV !== "production",
					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);
};

/**
 * Compile template to render function.
 * 生成 render 渲染函数
 */
export const createCompiler = createCompilerCreator(function baseCompile(
	template: string,
	options: CompilerOptions
): CompiledResult {
	// 模板解析节点,用正则等方式解析 template 模板中的指令,class、style等数据生成 AST
	const ast = parse(template.trim(), options);
	if (options.optimize !== false) {
		// 第二阶段 —— 优化,遍历 AST 找出其中的静态节点,并添加标记
		optimize(ast, options);
	}
	// 第三阶段 —— 代码生成阶段,将 AST 转换成 render 渲染函数
	const code = generate(ast, options);
	return {
		ast,
		render: code.render,
		staticRenderFns: code.staticRenderFns,
	};
});

2025-06-29-22-39-23-image.png

2025-06-30-22-23-08-image.png