likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 7

transform

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

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上下文。

/**
 * 对给定的根节点进行转换处理。
 * @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 节点

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来定义这些唯一标识符

export const TO_DISPLAY_STRING = Symbol(`toDisplayString`);

第三步,根节点生成 codegenNode:

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;
  }
}

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

  /**
 * 对给定的根节点进行转换处理。
 * @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模块开发完成。

转载自:https://juejin.cn/post/7356175306938564644
评论
请登录