模板编译原理
下面,我们来系统的梳理关于 Vue模板编译原理 的基本知识点:
一、模板编译概述
1.1 什么是模板编译
Vue的模板编译是将开发者编写的模板字符串转换为可执行的渲染函数(render function)的过程。这个过程包括:
- 解析模板字符串生成AST(抽象语法树)
- 对AST进行优化处理
- 将优化后的AST转换为渲染函数代码
1.2 编译过程三个阶段
- 解析阶段(Parse):将模板字符串解析为AST
- 优化阶段(Optimize):遍历AST,标记静态节点
- 代码生成阶段(Codegen):根据AST生成渲染函数代码
1.3 整体工作流程
模板字符串 → [解析器] → AST → [优化器] → 优化后的AST → [代码生成器] → 渲染函数
二、解析阶段(Parse):模板字符串 → AST
2.1 AST节点结构
AST节点是描述模板中各部分结构的JavaScript对象。常见的节点类型:
// 元素节点
{type: 1, // 节点类型(1:元素,2:动态文本,3:纯文本)tag: 'div', // 标签名attrsList: [{ name: 'id', value: 'app' }], // 属性列表attrsMap: { id: 'app' }, // 属性映射parent: null, // 父节点children: [], // 子节点// 特殊属性directives: [], // 指令events: {}, // 事件// 其他static: false, // 是否静态节点staticRoot: false // 是否静态根节点
}// 文本节点
{type: 2, // 动态文本expression: '_s(name)', // 表达式text: '{{ name }}' // 原始文本
}// 纯文本节点
{type: 3,text: 'Hello World'
}
2.2 解析器工作原理
解析器使用正则表达式和状态机技术,通过循环解析模板字符串,根据特定的规则(如开始标签、结束标签、文本等)将模板分解为令牌(token),然后构建AST树。
核心解析流程:
- 解析开始标签(包括属性和指令)
- 处理文本内容(包括插值表达式)
- 解析结束标签
- 构建节点间的父子关系
2.3 关键解析函数(伪代码)
function parse(template) {const stack = []; // 用于维护元素层级关系let root; // AST根节点let currentParent; // 当前父节点parseHTML(template, {// 处理开始标签start(tag, attrs, unary) {const element = createASTElement(tag, attrs);// 处理根节点if (!root) root = element;// 建立父子关系if (currentParent) {currentParent.children.push(element);element.parent = currentParent;}// 非自闭合标签入栈if (!unary) {currentParent = element;stack.push(element);}},// 处理结束标签end() {stack.pop();currentParent = stack[stack.length - 1];},// 处理文本内容chars(text) {if (!currentParent) return;const children = currentParent.children;// 处理文本节点if (text.trim()) {let res;// 解析动态文本(插值表达式)if (text !== ' ' && (res = parseText(text))) {children.push({type: 2,expression: res.expression,text});} else {children.push({type: 3,text});}}}});return root;
}
2.4 特殊语法解析
- 指令解析:v-if, v-for等特殊指令会被解析为节点上的特殊属性
- 插值表达式:{{ message }} 会被解析为动态文本节点
- 注释节点: 会被忽略或保留(根据配置)
三、优化阶段(Optimize):标记静态节点
3.1 优化目的
- 跳过静态子树的重渲染,提高性能
- 在后续patch过程中直接复用静态节点
3.2 标记过程
- 递归遍历AST
- 标记静态节点(node.static = true)
- 标记静态根节点(node.staticRoot = true)
function optimize(root) {if (!root) return;// 第一步:标记所有静态节点markStatic(root);// 第二步:标记静态根节点markStaticRoots(root);
}function markStatic(node) {node.static = isStatic(node);if (node.type === 1) { // 元素节点for (let i = 0; i < node.children.length; i++) {const child = node.children[i];markStatic(child);if (!child.static) {node.static = false;}}}
}// 判断节点是否为静态
function isStatic(node) {if (node.type === 2) { // 带变量的动态文本节点return false;}if (node.type === 3) { // 纯文本节点return true;}// 元素节点需满足:// 1. 没有动态绑定(v-if, v-for等)// 2. 没有使用组件// 3. 所有子节点都是静态的return !node.if && !node.for &&!node.hasBindings && !node.tag.includes('-') && // 非组件Object.keys(node).every(isStaticKey);
}// 标记静态根节点
function markStaticRoots(node) {if (node.type === 1) {// 静态根节点需要满足:是静态节点且有子节点,且子节点不全是文本节点if (node.static && node.children.length && !(node.children.length === 1 && node.children[0].type === 3)) {node.staticRoot = true;return;} else {node.staticRoot = false;}node.children.forEach(markStaticRoots);}
}
四、代码生成阶段(Codegen):AST → Render函数
4.1 代码生成原理
遍历AST,递归生成描述虚拟DOM的JavaScript代码字符串,最终拼接成完整的渲染函数。
4.2 核心生成函数
function generate(ast) {const code = ast ? genElement(ast) : '_c("div")';return {render: `with(this){return ${code}}`,staticRenderFns: state.staticRenderFns};
}// 生成元素节点
function genElement(el) {// 处理静态根节点if (el.staticRoot && !el.staticProcessed) {return genStatic(el);}// 生成数据对象(属性、指令等)const data = genData(el);// 生成子节点const children = genChildren(el);return `_c('${el.tag}'${data ? `,${data}` : '' // 数据}${children ? `,${children}` : '' // 子节点})`;
}// 生成子节点数组
function genChildren(el) {const children = el.children;if (children.length) {return `[${children.map(genNode).join(',')}]`;}
}// 生成节点
function genNode(node) {if (node.type === 1) {return genElement(node);} else if (node.type === 3 && node.isComment) {return genComment(node);} else {return genText(node);}
}// 生成文本节点
function genText(text) {return `_v(${text.type === 2? text.expression // 动态文本: _s(name): JSON.stringify(text.text) // 静态文本})`;
}
4.3 生成的渲染函数示例
模板:
<div id="app"><p>{{ message }}</p><button @click="handleClick">Click</button>
</div>
生成的渲染函数:
with(this) {return _c('div', { attrs: { "id": "app" } }, [_c('p', [_v(_s(message))]),_c('button', { on: { "click": handleClick } }, [_v("Click")])])
}
4.4 渲染函数中的核心方法
_c
: 创建虚拟节点(createElement)_v
: 创建文本虚拟节点(createTextVNode)_s
: 转换为字符串(toString)_l
: 渲染列表(renderList)_e
: 创建空节点(createEmptyVNode)
五、完整编译流程示例
5.1 输入模板
<div id="app"><h1>{{ title }}</h1><ul><li v-for="item in items" :key="item.id">{{ item.name }}</li></ul><button @click="addItem">Add</button>
</div>
5.2 生成AST(简化版)
{"type": 1,"tag": "div","attrsList": [{"name": "id", "value": "app"}],"attrsMap": {"id": "app"},"children": [{"type": 1,"tag": "h1","children": [{"type": 2, "expression": "_s(title)", "text": "{{ title }}"}]},{"type": 1,"tag": "ul","children": [{"type": 1,"tag": "li","attrsList": [{"name": "v-for", "value": "item in items"}],"for": "items","alias": "item","key": "item.id","children": [{"type": 2, "expression": "_s(item.name)", "text": "{{ item.name }}"}]}]},{"type": 1,"tag": "button","events": {"click": "addItem"},"children": [{"type": 3, "text": "Add"}]}]
}
5.3 优化后AST
- div节点:静态根节点(staticRoot: true)
- ul节点:动态节点(包含v-for)
- button节点:动态节点(包含事件)
5.4 生成渲染函数
with(this) {return _c('div', { attrs: { "id": "app" } }, [_c('h1', [_v(_s(title))]),_c('ul', _l((items), function(item) {return _c('li', { key: item.id }, [_v(_s(item.name))])})),_c('button', { on: { "click": addItem } }, [_v("Add")])])
}
六、关键技术与设计思想
6.1 使用栈管理节点层级
解析器使用栈结构维护标签的嵌套关系,确保正确构建AST的父子结构
6.2 静态节点优化
通过标记静态节点,避免不必要的重渲染,大幅提升性能
6.3 生成代码的设计
- 使用
with(this)
扩展作用域,方便访问组件实例属性 - 通过简洁的虚拟节点创建函数构建虚拟DOM树
- 分离静态渲染函数,优化性能
七、Vue 2 vs Vue 3 模板编译对比
特性 | Vue 2 | Vue 3 |
---|---|---|
AST结构 | 较简单 | 更详细,包含更多编译信息 |
优化策略 | 静态节点标记 | 更精细的静态提升(hoistStatic) |
代码生成 | 单一渲染函数 | 可能生成多个渲染函数 |
编译时优化 | 较少 | 更多编译时优化(如patchFlag) |
源码组织 | 集中式 | 模块化(@vue/compiler-core) |
输出格式 | 单一渲染函数 | 可能包含多个渲染函数和静态节点 |
八、学习建议与实践
8.1 学习路径
- 理解AST结构及其表示方式
- 研究解析器如何拆分模板字符串
- 掌握优化阶段静态标记的原理
- 学习代码生成器的递归生成策略
- 熟悉渲染函数的结构和用法
8.2 调试建议
- 使用Vue Template Explorer工具观察模板编译结果
- 在Vue源码中调试compiler模块
- 尝试编写简单的模板编译器
8.3 实现:迷你模板编译器
// 简化的模板编译器
function compile(template) {// 解析为ASTconst ast = parse(template);// 生成代码const code = generate(ast);return new Function(`with(this){return ${code}}`);
}// 简化版解析器
function parse(template) {// 正则表达式匹配标签和属性const tagMatch = template.match(/<(\w+)/);if (!tagMatch) return null;const root = {type: 1,tag: tagMatch[1],children: []};// 处理属性const attrMatch = template.match(/\s+(\w+)=['"]([^'"]+)['"]/);if (attrMatch) {root.attrsList = [{ name: attrMatch[1], value: attrMatch[2] }];}// 处理文本内容const textMatch = template.match(/>([^<]+)</);if (textMatch) {const text = textMatch[1].trim();if (text) {// 判断是否是动态文本if (text.includes('{{')) {const exp = text.replace(/{{([^}]+)}}/, (_, p1) => p1.trim());root.children.push({type: 2,expression: `_s(${exp})`,text});} else {root.children.push({type: 3,text});}}}return root;
}// 简化版代码生成器
function generate(ast) {if (ast.type === 1) {const data = [];// 处理属性if (ast.attrsList) {const attrs = ast.attrsList.map(attr => `${JSON.stringify(attr.name)}:${JSON.stringify(attr.value)}`).join(',');data.push(`attrs:{${attrs}}`);}// 处理子节点const children = ast.children.map(child => {if (child.type === 2) {return `_v(${child.expression})`;} else {return `_v(${JSON.stringify(child.text)})`;}}).join(',');return `_c('${ast.tag}', ${data.length ? `{${data}}` : 'null'}, [${children}])`;}return '';
}// 使用示例
const renderFn = compile('<div id="app">{{ message }}</div>');
console.log(renderFn.toString());
// 输出: function anonymous() {
// with(this){return _c('div', {attrs:{"id":"app"}}, [_v(_s(message))])}
// }