从零开始搭建简易Vue框架——(五)complier-core中的transform模块

transform

前面的文章我们已经完成了模版字符串到AST(抽象语法树)的转变。下面要做的是对AST进行润色。也就是源码中的transform模块。

js 复制代码
import { baseParse } from "./parse";

/**
 * 主编译函数,用于将模板字符串编译成渲染函数代码。
 * @param template 模板字符串,待编译的HTML模板。
 * @param options 编译选项,可配置额外的处理逻辑。
 * @returns 返回编译后的渲染函数代码字符串。
 */
export function baseCompile(template, options) {
  // 1. 将模板字符串解析成抽象语法树(AST)
  const ast = baseParse(template);

  // 2. 对AST进行转换处理,增强AST的功能和表现
  transform(
    ast,
    Object.assign(options, {
      nodeTransforms: [transformElement, transformText, transformExpression],
    })
  );
}

transform的基本实现

在我们的compiler-core中新建transform.ts文件,第一步,创建transform context上下文。

js 复制代码
/**
 * 对给定的根节点进行转换处理。
 * @param root 根节点,表示转换的起点。
 * @param options 可选参数对象,用于配置转换过程。
 */
export function transform(root, options = {}) {
  // 1. 创建转换上下文
  const context = createTransformContext(root, options);
}

/**
 * 创建一个转换上下文对象,用于辅助节点转换过程的管理。
 * @param root 根节点,表示转换的起点。
 * @param options 配置选项,可包含节点转换函数等。
 * @returns 返回一个包含转换上下文信息的对象。
 */
function createTransformContext(root, options): any {
  // 初始化转换上下文
  const context = {
    root,
    nodeTransforms: options.nodeTransforms || [],
    helpers: new Map(),
    helper(name) {
      // 这里会收集调用的次数
      // 收集次数是为了给删除做处理的, (当只有 count 为0 的时候才需要真的删除掉)
      // helpers 数据会在后续生成代码的时候用到
      const count = context.helpers.get(name) || 0;
      context.helpers.set(name, count + 1);
    },
  };

  return context;
}

第二步,遍历node 节点

js 复制代码
import { NodeTypes } from "./ast";

/**
 * 对给定的根节点进行转换处理。
 * @param root 根节点,表示转换的起点。
 * @param options 可选参数对象,用于配置转换过程。
 */
export function transform(root, options = {}) {
  // 1. 创建转换上下文
  const context = createTransformContext(root, options);
  // 2. 遍历 node
  traverseNode(root, context);
}

/**
 * 遍历给定的节点,并根据节点类型执行相应的处理逻辑。
 * @param node 要遍历的节点。
 * @param context 上下文对象,包含遍历过程中的状态和工具函数。
 */
function traverseNode(node, context) {
  // 获取节点类型
  const type: NodeTypes = node.type;
  // 获取上下文中定义的节点转换函数
  const nodeTransforms = context.nodeTransforms;
  const exitFns: any = [];
  // 对每个节点转换函数执行,并收集退出时需要调用的函数
  for (let i = 0; i < nodeTransforms.length; i++) {
    const transform = nodeTransforms[i];

    const onExit = transform(node, context);
    // 如果转换函数返回了退出函数,则收集起来
    if (onExit) {
      exitFns.push(onExit);
    }
  }

  // 根据节点类型执行相应的处理逻辑
  switch (type) {
    case NodeTypes.INTERPOLATION:
      // 插值的点,在于后续生成 render 代码的时候是获取变量的值
      context.helper(TO_DISPLAY_STRING);
      break;

    case NodeTypes.ROOT:
    case NodeTypes.ELEMENT:
      // 递归处理子节点
      traverseChildren(node, context);
      break;

    default:
      break;
  }

  let i = exitFns.length;
  // i-- 这个很巧妙
  // 使用 while 是要比 for 快 (可以使用 https://jsbench.me/ 来测试一下)
  while (i--) {
    exitFns[i]();
  }
}

/**
 * 遍历给定父节点的所有子节点。
 * @param parent 父节点,其应包含一个children数组,用于遍历其子节点。
 * @param context 传递给子节点遍历过程的上下文信息,可用于共享状态或数据。
 */
function traverseChildren(parent: any, context: any) {
  // node.children
  parent.children.forEach((node) => {
    traverseNode(node, context);
  });
}

/**
 * 创建一个转换上下文对象,用于辅助节点转换过程的管理。
 * @param root 根节点,表示转换的起点。
 * @param options 配置选项,可包含节点转换函数等。
 * @returns 返回一个包含转换上下文信息的对象。
 */
function createTransformContext(root, options): any {
  // 初始化转换上下文
  const context = {
    root,
    nodeTransforms: options.nodeTransforms || [],
    helpers: new Map(),
    helper(name) {
      // 这里会收集调用的次数
      // 收集次数是为了给删除做处理的, (当只有 count 为0 的时候才需要真的删除掉)
      // helpers 数据会在后续生成代码的时候用到
      const count = context.helpers.get(name) || 0;
      context.helpers.set(name, count + 1);
    },
  };

  return context;
}

注意:这里我们用到了TO_DISPLAY_STRING,这个变量要设置为不可变的唯一变量,所以我们新建runtimeHelper.ts来定义这些唯一标识符

js 复制代码
export const TO_DISPLAY_STRING = Symbol(`toDisplayString`);

第三步,根节点生成 codegenNode:

js 复制代码
export function transform(root, options = {}) {
  // 1. 创建转换上下文
  const context = createTransformContext(root, options);

  // 2. 遍历 node
  traverseNode(root, context);

  // 3. 根节点生成 codegenNode
  createRootCodegen(root, context);
}

/**
 * 创建根代码生成节点
 * @param root 根节点,包含子节点信息
 * @param context 上下文信息,此处未使用
 */
function createRootCodegen(root: any, context: any) {
  const { children } = root;

  // 只支持有一个根节点, ,并且该根节点必须是一个单文本节点
  // 是一个 single text node
  const child = children[0];

  // 如果是 element 类型的话 , 那么我们需要把它的 codegenNode 赋值给 root
  // root 其实是个空的什么数据都没有的节点
  // 所以这里需要额外的处理 codegenNode
  // codegenNode 的目的是专门为了 codegen 准备的  为的就是和 ast 的 node 分离开
  if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
    const codegenNode = child.codegenNode;
    root.codegenNode = codegenNode;
  } else {
    // 如果子节点不是元素类型或没有代码生成节点,则直接将子节点赋值给根节点的代码生成节点
    root.codegenNode = child;
  }
}

第四步,将上下文中的帮助函数添加到根节点的辅助函数列表中

js 复制代码
  /**
 * 对给定的根节点进行转换处理。
 * @param root 根节点,表示转换的起点。
 * @param options 可选参数对象,用于配置转换过程。
 */
export function transform(root, options = {}) {
  // 1. 创建转换上下文
  const context = createTransformContext(root, options);

  // 2. 遍历 node
  traverseNode(root, context);

  // 3. 根节点生成 codegenNode
  createRootCodegen(root, context);

  // 4. 将上下文中的帮助函数添加到根节点的辅助函数列表中
  root.helpers.push(...context.helpers.keys());
}

到此,transform模块开发完成。

相关推荐
10年前端老司机4 小时前
React无限级菜单:一个项目带你突破技术瓶颈
前端·javascript·react.js
阿芯爱编程8 小时前
2025前端面试题
前端·面试
前端小趴菜059 小时前
React - createPortal
前端·vue.js·react.js
晓131310 小时前
JavaScript加强篇——第四章 日期对象与DOM节点(基础)
开发语言·前端·javascript
菜包eo10 小时前
如何设置直播间的观看门槛,让直播间安全有效地运行?
前端·安全·音视频
烛阴10 小时前
JavaScript函数参数完全指南:从基础到高级技巧,一网打尽!
前端·javascript
chao_78911 小时前
frame 与新窗口切换操作【selenium 】
前端·javascript·css·selenium·测试工具·自动化·html
天蓝色的鱼鱼12 小时前
从零实现浏览器摄像头控制与视频录制:基于原生 JavaScript 的完整指南
前端·javascript
三原12 小时前
7000块帮朋友做了2个小程序加一个后台管理系统,值不值?
前端·vue.js·微信小程序
popoxf12 小时前
在新版本的微信开发者工具中使用npm包
前端·npm·node.js