如何实现模版引擎

文章目录

  • 一、核心目标
  • [二、关键步骤(参考 Vue 的编译流程)](#二、关键步骤(参考 Vue 的编译流程))
    • [1. 解析阶段(Parse):模板 → AST](#1. 解析阶段(Parse):模板 → AST)
    • [2. 转换阶段(Transform):AST → 优化后的 AST](#2. 转换阶段(Transform):AST → 优化后的 AST)
    • [3. 生成阶段(Generate):AST → 渲染函数](#3. 生成阶段(Generate):AST → 渲染函数)
    • [4. 渲染与更新:VNode → DOM + 响应式联动](#4. 渲染与更新:VNode → DOM + 响应式联动)
  • 三、简化版实现示例(核心逻辑串联)
  • [四、与 Vue 的核心差异(简化版 vs 真实实现)](#四、与 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)。

示例:

模板:

javascript 复制代码
<div id="app">
  <p v-if="show">Hello {{ name }}</p>
</div>

解析后的 AST(简化):

javascript 复制代码
{
  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(简化):

javascript 复制代码
{
  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(虚拟节点)。例如:
javascript 复制代码
// 生成的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:
javascript 复制代码
// 简化的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 算法)。

三、简化版实现示例(核心逻辑串联)

以下是一个极简模板引擎的核心代码,串联上述步骤:

javascript 复制代码
// 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 操作)。