从零开始搭建简易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模块开发完成。

相关推荐
天空属于哈夫克31 天前
企业微信二次开发:精准实现关键词自动回复
架构·企业微信
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
A黄俊辉A1 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱1 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
这个DBA有点耶1 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js