如何实现模版引擎
文章目录
- 一、核心目标
- 二、关键步骤(参考 Vue 的编译流程)
- 1. 解析阶段(Parse):模板 → AST
- 2. 转换阶段(Transform):AST → 优化后的 AST
- 3. 生成阶段(Generate):AST → 渲染函数
- 4. 渲染与更新:VNode → DOM + 响应式联动
- 三、简化版实现示例(核心逻辑串联)
- 四、与 Vue 的核心差异(简化版 vs 真实实现)
- 总结
实现一个模板引擎(参考 Vue 的思路)核心是将 “模板字符串” 转换为 “可执行的渲染逻辑”,并结合响应式系统实现 “数据驱动更新”。以下是核心思路和关键步骤,分阶段解析:
一、核心目标
模板引擎的核心目标是:将包含插值(如{{}})、指令(如v-if/v-for)的模板,转换为能根据数据动态生成 DOM 的逻辑,并在数据变化时自动更新 DOM
。
二、关键步骤(参考 Vue 的编译流程)
Vue 的模板引擎(compiler)分为 3 个阶段:解析(Parse)→ 转换(Transform)→ 生成(Generate),最终产出渲染函数(render function)。我们以此为框架展开:
1. 解析阶段(Parse):模板 → AST
目标:将字符串模板解析为结构化的抽象语法树(AST),方便后续处理。
AST 是用 JavaScript 对象描述模板的层级结构,包含标签、属性、文本、指令等信息。
-
需要处理的模板元素
普通标签(如<div>
、<span>
)
文本节点(如Hello
)
插值(如{{ message }}
)
指令(如v-if="show"
、v-for="item in list"
)
事件绑定(如@click="handleClick"
) -
解析逻辑(简化版)
用 “状态机” 逐字符扫描模板,识别不同的语法结构:
标签解析:遇到<
时进入 “标签解析状态”,提取标签名、属性(如id="app"
),遇到>
结束标签开始。
文本解析:非标签区域为文本,若包含{{
则识别为 “插值文本”,否则为 “纯文本”。
指令解析:对标签属性中以v-
开头的属性(如v-if
),单独标记为指令,记录指令名和表达式(如show
)。
示例:
模板:
<div id="app"><p v-if="show">Hello {{ name }}</p>
</div>
解析后的 AST(简化):
{type: 'ELEMENT', // 元素节点tag: 'div',attrs: [{ name: 'id', value: 'app' }],children: [{type: 'ELEMENT',tag: 'p',directives: [{ name: 'if', exp: 'show' }], // v-if指令children: [{type: 'TEXT', // 文本节点content: 'Hello ',},{type: 'INTERPOLATION', // 插值节点exp: 'name' // 绑定的变量}]}]
}
2. 转换阶段(Transform):AST → 优化后的 AST
目标:处理 AST 中的指令、插值等特殊语法,转换为可执行的逻辑,并做静态节点优化。
-
指令处理
v-if
:将节点转换为条件判断逻辑(如if (show) { ... }
)。
v-for
:将节点转换为循环逻辑(如list.forEach(item => { ... })
)。
事件绑定:将@click="handleClick"
转换为事件监听逻辑(如el.addEventListener('click', handleClick)
)。 -
静态节点优化
标记 “不会随数据变化的节点”(如纯文本<p>静态文本</p>
),避免在数据更新时重复渲染,提升性能。Vue 中会给静态节点添加isStatic: true
标记。
示例:
处理v-if后的 AST(简化):
{type: 'ELEMENT',tag: 'div',children: [{type: 'IF', // 转换为条件节点condition: 'show', // 条件表达式branch: { /* 原p标签的AST(当show为true时渲染) */ }}]
}
3. 生成阶段(Generate):AST → 渲染函数
目标:将优化后的 AST 转换为渲染函数(render function)—— 一段可执行的 JavaScript 代码,执行后生成虚拟 DOM(VNode)。
- 渲染函数的作用
渲染函数是模板的 “JavaScript
化”,它接收data
作为参数,返回描述 DOM 结构的 VNode(虚拟节点)
。例如:
// 生成的render函数(简化)
function render(data) {//其中h是创建 VNode 的函数(类似 Vue 的createVNode)。return h('div', { id: 'app' }, [data.show ? h('p', null, ['Hello ', data.name]) : null]);
}
- 代码生成逻辑
遍历 AST,将不同类型的节点转换为对应的h函数调用:
元素节点:h(tag, props, children)
文本节点:直接返回文本内容
插值节点:data[exp](如data.name)
条件节点:condition ? 分支1 : 分支2
循环节点:list.map(item => h(…))
4. 渲染与更新:VNode → DOM + 响应式联动
生成渲染函数后,还需要实现 “将 VNode 转换为真实 DOM
” 以及 “数据变化时自动更新
” 的逻辑。
- VNode 与真实 DOM 的映射
VNode 是对 DOM 的轻量描述(包含tag、props、children等),通过patch函数将 VNode 转换为真实 DOM:
// 简化的patch函数:将VNode渲染为真实DOM
function patch(vnode, container) {if (typeof vnode === 'string') { // 文本节点container.textContent = vnode;return;}const el = document.createElement(vnode.tag); // 创建元素// 设置属性Object.entries(vnode.props || {}).forEach(([key, value]) => {el.setAttribute(key, value);});// 递归处理子节点vnode.children.forEach(child => patch(child, el));container.appendChild(el);
}
- 响应式集成(核心!)
为了实现 “数据变,DOM 自动变
”,需要在渲染函数执行时收集依赖,数据变化时触发重新渲染
:
依赖收集:当渲染函数访问data.name
时,通过响应式系统(如Proxy)记录 “这个渲染函数依赖name”。
触发更新:当data.name
变化时,响应式系统通知所有依赖它的渲染函数重新执行,生成新的 VNode,再通过patch对比新旧 VNode,只更新变化的 DOM 部分(diff 算法)。
三、简化版实现示例(核心逻辑串联)
以下是一个极简模板引擎的核心代码,串联上述步骤:
// 1. 解析阶段:模板 → AST(简化版,仅处理插值和简单标签)
function parse(template) {// 简化处理:假设模板是单一根元素,包含插值const ast = { type: 'ELEMENT', tag: 'div', children: [] };// 匹配{{ }}插值const interpolationRegex = /{{\s*(\w+)\s*}}/g;const text = template.replace(/<[^>]+>/g, '').trim(); // 提取文本内容if (interpolationRegex.test(text)) {// 拆分纯文本和插值const parts = text.split(interpolationRegex);parts.forEach((part, index) => {if (index % 2 === 0 && part) { // 纯文本ast.children.push({ type: 'TEXT', content: part });} else if (index % 2 === 1) { // 插值ast.children.push({ type: 'INTERPOLATION', exp: part });}});} else {ast.children.push({ type: 'TEXT', content: text });}return ast;
}// 2. 转换阶段:AST → 优化AST(简化版,处理插值)
function transform(ast) {// 遍历AST,标记动态节点(含插值的节点)function traverse(node) {if (node.type === 'INTERPOLATION') {node.isDynamic = true; // 标记为动态节点}if (node.children) {node.children.forEach(traverse);}}traverse(ast);return ast;
}// 3. 生成阶段:AST → 渲染函数
function generate(ast) {// 生成children的代码const generateChildren = (children) => {return children.map(child => {if (child.type === 'TEXT') {return `'${child.content}'`; // 纯文本直接返回字符串}if (child.type === 'INTERPOLATION') {return `data.${child.exp}`; // 插值对应data中的属性}}).join(', ');};const childrenCode = generateChildren(ast.children);// 生成render函数字符串const code = `function render(data) {return {tag: '${ast.tag}',children: [${childrenCode}]};}`;// 执行字符串,返回render函数return new Function(code)();
}// 4. 响应式系统(简化版,基于Proxy)
function reactive(data) {const deps = new Set(); // 依赖集合(存放渲染函数)const proxy = new Proxy(data, {get(target, key) {// 收集依赖:当前执行的渲染函数if (activeEffect) deps.add(activeEffect);return target[key];},set(target, key, value) {target[key] = value;// 触发更新:执行所有依赖deps.forEach(effect => effect());}});return proxy;
}// 5. 渲染与更新
let activeEffect = null;
function mount(render, data, container) {// 定义副作用:执行render并更新DOMconst effect = () => {const vnode = render(data); // 生成VNodepatch(vnode, container); // 渲染到DOM};activeEffect = effect;effect(); // 首次渲染activeEffect = null;
}// 简化的patch函数:将VNode渲染到容器
function patch(vnode, container) {container.innerHTML = ''; // 清空容器if (vnode.tag) {const el = document.createElement(vnode.tag);// 处理子节点(文本或插值)vnode.children.forEach(child => {const textNode = document.createTextNode(child);el.appendChild(textNode);});container.appendChild(el);}
}// ------------ 使用示例 ------------
const template = `<div>Hello {{ name }}!</div>
`;// 编译流程
const ast = parse(template);
const transformedAst = transform(ast);
const render = generate(transformedAst);// 响应式数据
const data = reactive({ name: 'Vue' });// 挂载到页面
mount(render, data, document.getElementById('app'));// 3秒后修改数据,触发自动更新
setTimeout(() => {data.name = 'Template Engine'; // DOM会自动更新为"Hello Template Engine!"
}, 3000);
四、与 Vue 的核心差异(简化版 vs 真实实现)
解析能力:真实 Vue 的解析器能处理复杂 HTML(嵌套标签、自闭合标签、DOCTYPE 等),且用更严谨的状态机避免 XSS 风险。
优化程度:Vue 会标记静态根节点、预编译静态内容,减少渲染函数体积和执行时间。
Diff 算法:Vue 的patch函数使用高效的虚拟 DOM 对比算法(同层比较、key 复用),只更新变化的 DOM 节点。
指令丰富度:支持v-model、v-bind、v-slot等复杂指令,转换阶段会生成对应的逻辑。
总结
实现模板引擎的核心思路是 “模板→AST→渲染函数→VNode→DOM”
的流水线,结合响应式系统实现 “数据驱动更新”。Vue 的高明之处在于:通过编译阶段的优化(静态节点标记)和运行时的高效 diff 算法,平衡了开发体验(模板的直观性)和性能(最小化 DOM 操作)。