Skip to content

transform 流程

在 parse 阶段通过光标偏移分析将整个代码文本内容转换成了一个 NodeType 类型树,那么 transform 的流程就是对这颗树进行分析和针对 Vue 内容的一些优化,最终交给 codegen 阶段处理。例外这是一个平台差异最大的一个流程。

整体流程

对于 transform 的流程,我们可以将其看做一个洋葱圈模型, 在深度优先变量 AST 树的时候,对于每一个节点类型都通过 nodeTransforms 数组中从前到后依次执行,并将每一个执行的结果缓存到 exitFuncs 数组中,再从后向前执行。 其图形如下

image-20230303171156296

下面具体分析

  1. 合并各个环境 transform 的钩子函数
ts
// 生成针对Node 和 针对属性 的transform方法
const [nodeTransforms, directiveTransforms] =
  getBaseTransformPreset(prefixIdentifiers);

if (!__BROWSER__ && options.isTS) {
  const { expressionPlugins } = options;
  if (!expressionPlugins || !expressionPlugins.includes("typescript")) {
    options.expressionPlugins = [...(expressionPlugins || []), "typescript"];
  }
}

这样就得到结构如下的两个数组

js
nodeTransforms = [
  transformOnce,
  transformIf,
  transformMemo,
  transformFor,
  ...[],
  ...[transformExpression],
  transformSlotOutlet,
  transformElement,
  trackSlotScopes,
  transformText,
];
js
directiveTransforms = {
  on: transformOn,
  bind: transformBind,
  model: transformModel,
};

洋葱圈转换流程

ts
/**
 * 处理 AST 对象
 * @param node
 * @param context
 * @returns
 */
export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  // 指向当前 node
  context.currentNode = node;
  // apply transform plugins
  const { nodeTransforms } = context;
  // 洋葱圈 从后向前 执行回调数组
  const exitFns = [];
  // 遍历所有的transform函数并进行回调处理
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context);
    // 如果存在返回体,就将返回值放到 exitFus 数组中
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit);
      } else {
        exitFns.push(onExit);
      }
    }
    if (!context.currentNode) {
      // node was removed
      return;
    } else {
      // node may have been replaced
      node = context.currentNode;
    }
  }

  // exit transforms
  context.currentNode = node;
  let i = exitFns.length;
  while (i--) {
    exitFns[i]();
  }
}

codegenNode 对象

这个步骤其实除了 IF FOR 等破坏 AST 语法树结构的 transform,其他 transform 转换的内容其实没有作用于 AST 对象上,而是将其保留结构的情况下放在了 AST 对象的 codegenNode 属性上,这样

  1. 通过 AST 语法树维护了原生的树结构 。
  2. 通过 codegenNode 的判断对编译做一个提升 。比如:

例如: transformText

html
<div>{{ val }} -- {{num}}</div>

AST 树:

image-20230306162930868

codegenNode 树:

image-20230306163300861

重点:

  1. 发现 codegenNode 的 children 不一定是数组类型的了,可能变成了一个对象类型

  2. 为什么需要洋葱圈模型?

仔细观察发现对于 transform 内置的转换插件,其可以分为两大类:

  • 简单的内容转换,如 transformText、transformOn、transformVMemo、transformBind、transformElement 等
  • 结构化转换, 如 transformIf、transformFor

对于第二种会改变原来的 AST 语法树的结构,所以需要将其结果放到 exisFunc 重新处理,所以这里面就涉及到一个同一个节点上同时存在 v-ifv-for的优先级问题。

在 Vue2 的时候对于这些 transform 是在 parse 阶段就进行处理的,且 processFor 的处理时机是在 processIf 之前

在 Vue3 的时候,其借助于洋葱圈模型将这些破坏性处理的指令都存放在 exitFuns 中进行处理,从而避免了当前及后代属性的处理实际,同时因为 transformIf 在 transformFor 之前,那么 exitFuncIf 就在 exitFunFor 之后进行处理,这样执行的时候 if 的优先级就高于 for 的优先级了