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

相关推荐
雷神乐乐2 分钟前
创建前端项目的方法
前端·javascript·vue.js
prince_zxill8 分钟前
JavaScript面向对象编程:Prototype与Class的对比详解
前端·javascript·ecmascript·原型模式
计算机-秋大田40 分钟前
基于SpringBoot的美食烹饪互动平台的设计与实现(源码+SQL脚本+LW+部署讲解等)
vue.js·spring boot·后端·课程设计·美食
D.eL44 分钟前
Vue 2 项目中 Mock.js 的完整集成与使用教程
前端·javascript·vue.js
brzhang1 小时前
墙裂推荐一个在 Apple Silicon 上创建和管理虚拟机的轻量级开源工具:lume
前端·后端
轻口味2 小时前
Vue.js 响应式引用与响应式数据(`ref` 和 `reactive`)
vue.js
Along丶WG2 小时前
解决国内服务器 npm install 卡住的问题
前端·npm·node.js
prince_zxill2 小时前
Node.js 和 npm 安装教程
前端·javascript·vue.js·npm·node.js
弄不死的强仔3 小时前
可被electron等调用的Qt截图-录屏工具【源码开放】
前端·javascript·qt·electron·贴图·qt5
霸王蟹3 小时前
el-table组件样式如何二次修改?
前端·javascript·vue.js·笔记·学习·前端框架