Vue 2 - 模板编译源码理解

版本 :以 vue@2.7.16 代码为参考

概念理解

抽象语法树(AST)

抽象语法树:是源代码抽象结构表示,是模板语法和最终渲染代码的中间形态。通过抽象语法树可以完成代码压缩、代码转换、静态节点优化等操作。

AST 的作用

  • 代码转换:可以基于 AST 进行代码转换,如 Babel 兼容处理、代码压缩等
  • 代码生成:递归遍历 AST 树,重新生成最终的代码结果,拼接成完整代码
  • 静态分析:分析代码结构,进行静态节点优化、依赖分析等

AST 构建示例

javascript 复制代码
// 简化的 AST 构建示例
let ast = {
  type: "Program",
  body: [
    {
      type: "CallExpression",
      name: "add",
      params: [
        {
          type: "NumberLiteral",
          value: token.value,
        },
      ],
    },
  ],
};

// walk 是一个递归执行的函数
function walk() {
  let token = tokens[current];

  // 数字类型语法树转换
  if (token.type === "number") {
    return {
      type: "NumberLiteral",
      value: token.value,
    };
  }

  // 函数调用执行判断
  if (token.type === "paren" && token.value === "(") {
    let node = {
      type: "CallExpression",
      name: token.value,
      params: [],
    };
    token = tokens[++current];

    // 判断非结束括号时,递归执行,获取 params 参数
    while (token.type !== "paren" || token.value !== ")") {
      node.params.push(walk());
      token = tokens[++current];
    }

    return node;
  }
}

let current = 0;
while (current < tokens.length) {
  ast.body.push(walk());
}

最终构建成一棵 AST 语法树。

Vue 2 模板编译流程

Vue 2 将 template 转成渲染函数的过程包含四个主要步骤:

  1. 模板解析 :调用 parseHTML 把模板解析成抽象语法树,包括属性、事件、元素标签。
  2. 静态节点优化:标记抽象语法树中的静态节点,然后递归标记子节点,如果子节点不是静态节点,那么整个节点就标记非静态。
  3. 代码(字符串)生成 :把 AST 语法树转换为 render() 函数,同时生成 staticRenderFns 用于静态节点渲染(生成均为代码字符串而非实际执行代码),后续组件渲染时执行 render 函数返回 VNode。
  4. 函数创建【后续步骤,可忽略】:把第 3 步生成的代码字符串转换为真实函数返回。

如果是使用构建框架,编译阶段会在 vue-loader 插件内完成,这样可以减少运行时的编译流程

如果是直接使用 编译 + 运行时的 Vue 框架,那编译阶段就会在组件初始化的时候完成:

created 执行后,beforeMount 执行前,Vue 会改写 Vue.prototype.$mount 函数,加入模板编译的代码流程,然后再执行组件挂载流程 mountComponent()

核心编译函数

typescript 复制代码
// src/compiler/index.ts

export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1. 解析模板,得到抽象语法树
  const ast = parse(template.trim(), options);

  // 2. 静态节点编译优化
  if (options.optimize !== false) {
    optimize(ast, options);
  }

  // 3. 构建 AST,生成渲染函数代码
  const code = generate(ast, options);

  return {
    ast,
    render: code.render, // 渲染函数
    staticRenderFns: code.staticRenderFns, // 静态渲染函数
  };
});

1. 模板解析(Parse)

模板解析需要处理以下节点类型:

  • 文本节点:纯文本内容。
  • 开始标签:HTML 元素的开始标签。
  • 闭合标签:和开始标签分开处理。
  • HTML 注释节点<!-- 注释 -->
  • 条件注释<!-- [if IE]> --> 内容 <!--< ![endif] -->
  • DOCTYPE 节点<!DOCTYPE html>

调用 parseHTML,传入模板和处理函数:

  • start:创建 AST 元素节点,处理开始标签的 v-if、v-for、v-on 的动态绑定事件。
  • end:结束标签处理。
  • chars :创建 AST 文本节点,处理文本中的表达式节点。如果遇到文本节点,会调用 parseText 处理文本节点,如果存在 {``{ xxx | filter }} 过滤器,就会调用 parseFilter 处理过滤器情况。
  • comment:创建 AST 注释节点。

源码实现

typescript 复制代码
export function processFor(el: ASTElement) {
  let exp;
  if ((exp = getAndRemoveAttr(el, "v-for"))) {
    // 解析 v-for
    const res = parseFor(exp);
    if (res) {
      extend(el, res);
    } else if (__DEV__) {
      warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap["v-for"]);
    }
  }
}

// 解析 v-if 标签
function processIf(el) {
  const exp = getAndRemoveAttr(el, "v-if");
  if (exp) {
    el.if = exp;
    addIfCondition(el, {
      exp: exp,
      block: el,
    });
  } else {
    if (getAndRemoveAttr(el, "v-else") != null) {
      el.else = true;
    }
    const elseif = getAndRemoveAttr(el, "v-else-if");
    if (elseif) {
      el.elseif = elseif;
    }
  }
}

function makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> {
  const map = {};
  for (let i = 0, l = attrs.length; i < l; i++) {
    map[attrs[i].name] = attrs[i].value;
  }
  return map;
}

// AST 创建函数
export function createASTElement(
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: [],
  };
}

// src/compiler/parser/index.ts
export function parse(
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 解析 HTML,生成 AST 节点树
  parseHTML(template, {
    // 处理开始标签
    start(tag, attrs, unary, start, end) {
      // 创建 AST 元素节点
      let element: ASTElement = createASTElement(tag, attrs, currentParent);
      // 处理指令(v-if, v-for, v-on 等)
      processFor(element);
      processIf(element);
      processOnce(element);
      // ...
    },
    // 处理结束标签
    end(tag, start, end) {
      // 闭合标签,构建父子关系
    },
    // 处理文本
    chars(text: string, start: number, end: number) {
      // 创建文本节点或表达式节点
      // {{ message }} → 表达式节点
    },
    // 处理注释
    comment(text: string, start, end) {
      // 创建注释节点
    },
  });

  return root;
}
AST 节点类型

模板会被转换成一个个元素节点,分为元素节点、文本节点、表达式节点,不同节点类型用于优化数据结构,减少额外数据生成。

  • type 标记节点类型。
  • tag 表示标签字段。
  • children 表示子节点 AST 树。
typescript 复制代码
// 元素节点
interface ASTElement {
  type: 1;
  tag: string;
  attrsList: Array<{ name: string; value: any }>;
  attrsMap: { [key: string]: any };
  parent: ASTElement | null;
  children: Array<ASTNode>;
  // 指令相关
  if?: string;
  ifConditions?: Array<ASTIfCondition>;
  for?: string;
  forProcessed?: boolean;
  key?: string;
  // 静态标记
  static?: boolean;
  staticRoot?: boolean;
  // ...
}

// 文本节点
interface ASTText {
  type: 3;
  text: string;
  static?: boolean;
}

// 表达式节点
interface ASTExpression {
  type: 2;
  expression: string;
  text: string;
  static?: boolean;
}

示例'<div>{``{ message }}</div>' 解析为:

typescript 复制代码
{
  type: 1,
  tag: 'div',
  children: [{
    type: 2,
    expression: '_s(message)',
    text: '{{ message }}'
  }]
}

2. 静态节点优化(Optimize)

静态节点优化通过标记静态节点来提升渲染性能:

  • 如果是文本节点,标记为静态。
  • 如果是带表达式的节点,则标记为非静态。
  • 不包含 v-bind、v-if、v-for 等动态绑定标记,就标记为静态节点 node.static = true
  • 标记为静态节点并不代表节点就可以被编译优化,需要经过 markStaticRoots() 将其标记为根静态节点 node.staticRoot = true ,其代码才会在后续的 "代码生成" 阶段被存入静态渲染函数列表 staticRenderFns
typescript 复制代码
// src/compiler/optimizer.ts
export function optimize(
  root: ASTElement | null | undefined,
  options: CompilerOptions
) {
  if (!root) return;

  // 第一步:标记所有静态节点
  markStatic(root);

  // 第二步:标记静态根节点
  markStaticRoots(root, false);
}

function markStatic(node: ASTNode) {
  // 判断节点是否为静态
  node.static = isStatic(node);

  if (node.type === 1) {
    // 元素节点:递归标记子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i];
      markStatic(child);
      if (!child.static) {
        node.static = false; // 子节点非静态,父节点也非静态
      }
    }
  }
}

function markStaticRoots(node: ASTNode, isInFor: boolean) {
  // 前提是元素节点
  if (node.type === 1) {
    // 设置for静态节点标记
    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);
      }
    }
  }
}

function isStatic(node: ASTNode): boolean {
  if (node.type === 2) {
    // 表达式节点:非静态
    return false;
  }
  if (node.type === 3) {
    // 文本节点:静态
    return true;
  }
  // 元素节点:检查是否有动态绑定
  return !(
    (
      node.hasBindings || // 有绑定(v-bind, v-on 等)
      node.if || // v-if
      node.for || // v-for
      isBuiltInTag(node.tag) || // 内置标签(slot, component)
      isPlatformReservedTag(node.tag) === false || // 非平台保留标签
      node.directives.length || // 有指令
      node.staticInFor
    ) // 在 v-for 中
  );
}

3. 代码(字符串)生成(Generate)

根据 AST 标识的节点类型,结合渲染辅助函数,生成最终渲染函数代码字符串:

  • 根据节点属性 v-for、v-if、v-once、是否有静态标记、是否有组件,走不同的渲染函数生成流程。
  • 通过 with 修改作用域表达。
  • 生成 renderstaticRenderFns 静态渲染函数代码字符串。

渲染辅助函数

  • _ccreateElement,创建 VNode。
  • _vcreateTextVNode,创建文本节点。
  • _stoString,转换为字符串。
  • _lrenderList,渲染列表(v-for)。
  • _mrenderStatic,渲染静态节点。
  • _ecreateEmptyVNode,创建空节点。

源码实现

typescript 复制代码
// src/compiler/codegen/index.ts
function genStatic(el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true;
  // Some elements (templates) need to behave differently inside of a v-pre
  // node. All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre;
  if (el.pre) {
    state.pre = el.pre;
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
  state.pre = originalPreState;
  return `_m(${state.staticRenderFns.length - 1}${
    el.staticInFor ? ",true" : ""
  })`;
}

export function generate(
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options);

  // 生成代码
  const code = ast
    ? ast.tag === "script"
      ? "null"
      : genElement(ast, state) // 递归生成元素代码
    : '_c("div")';

  return {
    render: `with(this){return ${code}}`, // 包装为函数体
    staticRenderFns: state.staticRenderFns,
  };
}

export function genElement(el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    // 静态根节点
    return genStatic(el, state);
  } else if (el.once && !el.onceProcessed) {
    // v-once
    return genOnce(el, state);
  } else if (el.for && !el.forProcessed) {
    // v-for
    return genFor(el, state);
  } else if (el.if && !el.ifProcessed) {
    // v-if
    return genIf(el, state);
  } else if (el.tag === "slot") {
    // slot
    return genSlot(el, state);
  } else {
    // 普通元素或组件
    let code;
    if (el.component) {
      // 组件
      code = genComponent(el.component, el, state);
    } else {
      // 普通元素
      const data = el.plain ? undefined : genData(el, state);
      const children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = `_c('${el.tag}'${data ? `,${data}` : ""}${
        children ? `,${children}` : ""
      })`;
    }
    return code;
  }
}
生成代码示例
javascript 复制代码
// 模板
<div id="app" :class="className">
  <p>{{ message }}</p>
  <button @click="handleClick">Click</button>
</div>

// 生成的 render 函数代码
with(this) {
  return _c('div', {
    attrs: { "id": "app" },
    class: className
  }, [
    _c('p', [_v(_s(message))]),
    _c('button', {
      on: { "click": handleClick }
    }, [_v("Click")])
  ])
}

4. 函数创建

我觉得这一步其实才是实际代码生成阶段,创建将代码字符串编译成实际函数的编译方法

返回的编译函数会在传入的 VNode template 时,将代码字符串编译成实际的渲染函数并缓存

上述的 "代码生成" 阶段实际是代码字符串生成,没有转换成真正的函数

目前很多文章只是讲到 compile 函数完成时,没有讲最后的步骤

在完成 renderstaticRenderFns 渲染函数代码字符串生成后。

createCompileToFunctionFn() 函数内会调用 compile() 完成模板编译,然后将返回的代码字符串转换为函数

之后使用 new Function() 将字符串转换为函数,编译结果会被缓存,相同模板只编译一次,再后续初始化时直接从缓存中获取渲染函数即可。

typescript 复制代码
// src/compiler/to-function.ts
export function createCompileToFunctionFn(compile: Function): Function {
  const cache = Object.create(null); // 缓存编译结果

  return function compileToFunctions(
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 1. 检查缓存
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template;
    if (cache[key]) {
      return cache[key];
    }

    // 2. 编译模板
    const compiled = compile(template, options);

    // 3. 将代码字符串转换为函数
    const res: any = {};
    const fnGenErrors: any[] = [];
    // 把模板编译后返回的 render,转换成字符串返回
    res.render = createFunction(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map((code) => {
      return createFunction(code, fnGenErrors);
    });

    // 4. 缓存结果
    return (cache[key] = res);
  };
}

function createFunction(code, errors) {
  try {
    return new Function(code); // 使用 Function 构造函数创建函数
  } catch (err: any) {
    errors.push({ err, code });
    return noop;
  }
}

整体流程

bash 复制代码
Template: '<div>{{ message }}</div>'
    ↓
[Parse] 解析
    ↓
AST: { tag: 'div', children: [{ type: 2, expression: '_s(message)' }] }
    ↓
[Optimize] 优化
    ↓
AST: { tag: 'div', static: false, children: [{ static: false }] }
    ↓
[Generate] 生成代码
    ↓
Code: "with(this){return _c('div',[_v(_s(message))])}"
    ↓
[Create Function] 函数创建
    ↓
Render Function: function() { with(this){return _c('div',[_v(_s(message))])} }

思考

1. 为什么 Vue 2 使用 with 语句?

with 语句用于修改作用域,让 render 函数中的变量可以直接访问组件实例的属性,而不需要通过 this. 前缀。例如 _c('div') 可以直接访问,而不需要 this._c('div')

注意with 语句在严格模式下不可用,且会影响性能优化,Vue 3 已经移除了 with 语句。

2. 静态节点优化为什么能提升性能?

静态节点在 patch 更新时可以直接跳过 diff 比较,因为静态节点的内容不会改变。静态根节点创建一次后,后续直接复用,避免了重复创建 VNode 和 DOM 操作的开销。

总结

  • 核心流程:模板解析(Parse)→ 静态节点优化(Optimize)→ 代码生成(Generate)→ 函数创建。

    • AST 节点类型:元素节点(type: 1)、表达式节点(type: 2)、文本节点(type: 3)。
    • 静态节点优化:通过标记静态节点,在 patch 时跳过 diff,提升渲染性能。
    • 代码生成 :根据 AST 节点类型和指令,使用对应的渲染辅助函数 _c_v_s_l_m_e 包裹。
  • 优化点:

    • 静态节点优化减少不必要的 diff 操作。
    • 编译结果缓存避免重复编译。

参考内容

相关推荐
Irene19912 小时前
Vue:Props 和 Emits 对比总结
vue.js·props·emits
saadiya~2 小时前
实战笔记:在 Ubuntu 离线部署 Vue + Nginx 踩坑与避雷指南
vue.js·笔记·nginx
Thetimezipsby2 小时前
Redux、React Redux 快速学习上手、简单易懂、知识速查
前端·react.js·redux
被遗忘在角落的死小孩2 小时前
SSD 存储安全协议 TCG KPIO 笔记
笔记·安全
黎明初时2 小时前
react基础框架搭建2-准备工作:react+router+redux+axios+Tailwind+webpack
前端·react.js·webpack
弘毅 失败的 mian2 小时前
Git 远程操作
经验分享·笔记·git
threerocks2 小时前
我的年终总结 - 艰难的 2025
前端·面试·年终总结
随祥3 小时前
Tauri+vue开发桌面程序环境搭建
前端·javascript·vue.js
时空无限4 小时前
EFK 中使用 ruby 和 javascript 脚本去掉日志中颜色字符详解
linux·javascript·elk·ruby