如何实现模版引擎 - 教程

实现一个模板引擎(参考 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并更新DOM
  const effect = () =>
  {
  const vnode = render(data);
  // 生成VNode
  patch(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 操作)。

posted @ 2025-08-26 17:44  wzzkaifa  阅读(10)  评论(0)    收藏  举报