第 62 期 - Vue2 模板编译中生成器的原理与实践
logoFRONTALK AI/12月24日 16:42/阅读原文

摘要

文章详细阐述了 Vue2 模板编译里生成器将优化后的 AST 转换为渲染函数 JS 代码的过程,包括核心任务、入口方法、节点处理(以 v - if 和 v - for 为例)、子节点处理、静态节点处理等内容,还展示了整体架构流程。

1. 生成器概述

在 Vue2 模板编译中,生成器起着关键作用。它的主要任务是将优化后的抽象语法树(AST)转换为可执行的 JavaScript 代码,输出的是渲染函数,这个函数能够依据数据状态来创建或者更新 DOM 结构。例如对于模板<div v - if = "isShow"><li v - for = "item in items">{{item}}</li></div>,编译后的渲染函数字符串为with(this) {return (isShow)? _c('div', _l((items), function (item) {return _c('li', [_v(_s(item))])}), 0) : _e()},这里的_c_l_v_s_e等方法是 Vue 运行时的内置方法,属于运行时部分内容,不在本文重点讨论范围。

2. generate 入口方法

export function generate(
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 1. 创建 CodegenState 实例
  const state = new CodegenState(options);
  // 2. 根据 ast 情况确定 code
  const code = ast
   ? ast.tag === 'script'
     ? 'null'
      : genElement(ast, state)
    : '_c("div")';
  // 3. 返回包含渲染函数和静态渲染函数数组的对象
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  };
}

从这个入口方法可以看出,核心是通过genElement方法把 AST 转化为code字符串,最后利用with语句包装后作为render渲染函数字符串,同时返回对象中的staticRenderFns用于保存静态渲染函数字符串数组。这里的CodegenState实例可看作保存转换过程所需信息和状态的辅助类。

3. genElement 方法

export function genElement(el: ASTElement, state: CodegenState): string {
  // 一系列 if - else 判断处理不同情况
  if (el.parent) {
    el.pre = el.pre || el.parent.pre;
  }
  if (el.staticRoot &&!el.staticProcessed) {
    return genStatic(el, state);
  } else if (el.once &&!el.onceProcessed) {
    return genOnce(el, state);
  } else if (el.for &&!el.forProcessed) {
    return genFor(el, state);
  } else if (el.if &&!el.ifProcessed) {
    return genIf(el, state);
  } else if (el.tag === 'template' &&!el.slotTarget &&!state.pre) {
    return genChildren(el, state) || 'void 0';
  } else if (el.tag === 'slot') {
    return genSlot(el, state);
  } else {
    let code;
    let tag = `'${el.tag}'`;
    const children = genChildren(el, state, true);
    code = `_c(${tag}${
      data? `,${data}` : ''
    }${
      children? `,${children}` : ''
    })`;
    // 模块转换操作
    for (let i = 0; i < state.transforms.length; i ++) {
      code = state.transforms[i](el, code);
    }
    return code;
  }
}

这个方法首先进行很多if - else判断,遇到对应的内置指令后会进入专门分支处理。指令分支较多,文章以v - ifv - for指令为例来探索渲染函数的生成过程。判断之后针对子节点会调用genChildren方法生成子节点代码,最后返回根据_c(..)形式拼接而成的渲染函数字符串。

4. v - if 处理

对于模板中的v - if指令,如<div v - if = "isShow">//..</div>,在genElement方法中会进入else if (el.if &&!el.ifProcessed)这个分支,然后调用genIf函数。

export function genIf(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 1. 修改递归状态
  el.ifProcessed = true;
  // 2. 调用 genIfConditions 方法生成代码
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}

genIf函数中修改了递归状态后调用genIfConditions方法,传入的el.ifConditions.slice()是节点中的条件指令数组浅拷贝,避免后续处理影响原节点属性。genIfConditions方法根据条件数组情况生成相应代码,如果条件数组为空返回备用空节点字符串或者默认空节点_e();如果条件不为空则返回三元表达式,三元表达式的真假值部分会调用genTernaryExp方法,而genTernaryExp方法最终会调用genElement方法,由于前面设置了el.ifProcessed = true,所以不会再次进入genIf函数导致死循环。

5. v - for 处理

export function genFor(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for;
  const alias = el.alias;
  const iterator1 = el.iterator1? `,${el.iterator1}` : '';
  const iterator2 = el.iterator2? `,${el.iterator2}` : '';
  // 设置递归状态
  el.forProcessed = true;
  return (
    `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    '})'
  );
}

v - for逻辑相对v - if简单些。以<div v - for = "(value, name, index) in object"></div>为例,结合其转换后的 AST 节点来看genFor方法中的逻辑,先设置el.forProcessed = true避免递归时再次进入,然后按照逻辑生成相应代码。

6. genChildren 方法

export function genChildren(
  el: ASTElement,
  state: CodegenState
): string | void {
  const children = el.children;
  if (children.length) {
    const el: any = children[0];
    // 特殊情况处理
    if (
      children.length === 1 &&
      el.for &&
      el.tag!== 'template' &&
      el.tag!== 'slot'
    ) {
      return `${(genElement)(el, state)}`;
    }
    // 循环生成子节点代码
    return `[${children.map(c => genNode(c, state)).join(',')}]`;
  }
}

genChildren方法在genElement方法中被调用来生成子节点代码。如果children数组不为空,会根据不同情况处理,最后对子节点循环调用genNode方法。genNode方法根据 AST 节点类型分别调用对应方法生成具体代码,如genElement(元素节点)、genComment(注释节点)、genText(文本节点且非注释节点)。

7. 静态节点处理

对于优化器标记为静态根节点(staticRoot: true)的 AST 节点,会经过genStatic方法处理。

function genStatic(el: ASTElement, state: CodegenState): string {
  // 设置递归状态
  el.staticProcessed = true;
  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' : ''})`;
}

genStatic方法核心逻辑主要两步:一是将genElement产生的代码包装后保存到staticRenderFns数组,表示静态根节点对应的渲染函数代码;二是最终返回形如_m(0)的代码,参数是当前静态根节点在staticRenderFns数组中的索引下标。

8. 架构流程

文章最后将生成器源码整体串起来展示了架构流程,通过解析器、优化器和生成器这三个核心步骤,深入了解了 Vue2 如何将模板转换为高效的渲染函数。

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有