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

86 阅读6分钟

complier-core编译流程

要编译我们的html结构,首先就是要构建AST(抽象语法树)

import { baseParse } from "./parse";

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

complier-core中的核心逻辑的实现:

接下来我们来讲解一下complier-core中的核心逻辑parse编译模块的实现,也就是我们的baseParse函数的具体实现,首先要实现的是对模板字符串进行解析的模块,在源码中是被命名为parse,我们的命名也相同即可。先面试具体的实现过程:

import { ElementTypes, NodeTypes } from "./ast";

const enum TagType {
  Start,
  End,
}

/**
 * 解析给定的内容,并生成根节点。
 * @param content 要解析的字符串内容。
 * @returns 返回一个根节点,它包含了从给定字符串解析出的子节点。
 */
export function baseParse(content: string) {
  // 创建解析上下文
  const context = createParserContext(content);
  // 使用解析上下文解析子节点,并创建根节点返回
  return createRoot(parseChildren(context, []));
}

/**
 * 创建解析上下文
 * @param content 要解析的内容,类型为字符串
 * @return 返回一个包含源内容的解析上下文对象
 */
function createParserContext(content: string) {
  // 创建 parseContext
  return {
    source: content,
  };
}

/**
 * 解析子节点。
 * @param context 上下文对象,包含当前解析所需的所有信息。
 * @param ancestors 祖先节点数组,用于跟踪当前解析位置的父节点信息。
 */
function parseChildren(context, ancestors) {
  const nodes: any = []; // 存储解析过程中生成的节点

  while (!isEnd(context, ancestors)) {
    let node;
    const s = context.source;
    // 根据不同的起始标识符进行不同的解析处理
    if (startsWith(s, "{{")) {
      // 如果当前是以 {{ 开头的插值表达式,则进行插值解析
      node = parseInterpolation(context);
    } else if (s[0] === "<") {
      // 如果当前是以 < 开头的标签
      if (s[1] === "/") {
        // 如果是关闭标签
        if (/[a-z]/i.test(s[2])) {
          // 如果关闭标签是有效的,则进行解析,并继续当前循环
          parseTag(context, TagType.End);
          continue;
        }
      } else if (/[a-z]/i.test(s[1])) {
        // 如果是开始标签,则进行元素解析
        node = parseElement(context, ancestors);
      }
    }
  }
}

/**
 * 解析元素节点。
 * @param context 解析上下文,包含当前解析位置和源数据等信息。
 * @param ancestors 祖先元素节点数组,用于跟踪当前解析位置的父级元素。
 * @returns 返回解析后的元素节点,包含标签信息和子元素。
 */
function parseElement(context, ancestors) {
  // 解析起始标签
  const element = parseTag(context, TagType.Start);

  ancestors.push(element);
  // 解析子元素
  const children = parseChildren(context, ancestors);
  ancestors.pop();

  // 解析结束标签,以确保语法正确,并检查结束标签是否与起始标签匹配
  if (startsWithEndTagOpen(context.source, element?.tag)) {
    parseTag(context, TagType.End);
  } else {
    throw new Error(`缺失结束标签:${element?.tag}`);
  }

  // 为元素节点添加子元素
  element && (element["children"] = children);

  return element;
}

/**
 * 解析HTML标签。
 * @param context 上下文对象,包含当前解析的位置和源代码等信息。
 * @param type 标签类型,区分开始标签或结束标签。
 * @returns 如果是开始标签,返回一个包含标签信息的对象;如果是结束标签,则不返回任何内容。
 */
function parseTag(context, type: TagType) {
  // 使用正则表达式从源代码中匹配标签
  const match: any = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source);
  const tag = match[1];

  // 移动光标到标签名称后的位置,准备解析下一个字符
  advanceBy(context, match[0].length);

  // 跳过"<"符号,准备解析标签名称
  advanceBy(context, 1);

  // 如果是结束标签,则不进一步处理,直接返回
  if (type === TagType.End) return;

  // 默认标签类型为元素类型
  let tagType = ElementTypes.ELEMENT;

  // 返回解析出的标签信息
  return {
    type: NodeTypes.ELEMENT,
    tag,
    tagType,
  };
}

/**
 * 检查给定的上下文是否表示一个元素的结束。
 * @param context 包含源字符串的上下文对象。
 * @param ancestors 当前解析过程中遇到的祖先元素数组。
 * @returns {boolean} 如果当前上下文表示一个元素的结束,则返回true;否则返回false。
 */
function isEnd(context, ancestors) {
  const s = context.source;
  // 检查源字符串是否以结束标签开始
  if (s.startsWith("</")) {
    // 遍历祖先元素,寻找是否有匹配的结束标签
    for (let i = ancestors.length - 1; i >= 0; --i) {
      if (startsWithEndTagOpen(s, ancestors[i].tag)) {
        // 找到匹配的结束标签,返回true
        return true;
      }
    }
  }
}

/**
 * 检查给定的字符串源是否以特定的闭合标签开始。
 * @param source - 需要检查的字符串源。
 * @param tag - 指定的标签名称。
 * @returns 如果字符串源以指定的闭合标签开始,则返回true;否则返回false。
 */
function startsWithEndTagOpen(source: string, tag: string) {
  // 检查字符串源是否以 "</" 开头,并且接下来的部分与标签名称匹配。
  return (
    startsWith(source, "</") &&
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
  );
}

/**
 * 检查字符串是否以指定的前缀开始。
 * @param source - 被检查的源字符串。
 * @param searchString - 指定的前缀字符串。
 * @returns 返回一个布尔值,如果源字符串以指定的前缀开始,则为true;否则为false。
 */
function startsWith(source: string, searchString: string): boolean {
  return source.startsWith(searchString);
}

function parseInterpolation(context) {
  const openDelimiter = "{{";
  const closeDelimiter = "}}";

  const closeIndex = context.source.indexOf(
    closeDelimiter,
    openDelimiter.length
  );

  // 让代码前进2个长度,可以把 {{ 干掉
  advanceBy(context, 2);

  const rawContentLength = closeIndex - openDelimiter.length;
  const rawContent = context.source.slice(0, rawContentLength);

  const preTrimContent = parseTextData(context, rawContent.length);
  const content = preTrimContent.trim();

  // 最后在让代码前进2个长度,可以把 }} 干掉
  advanceBy(context, closeDelimiter.length);
}

/**
 * 解析文本数据
 * @param context 上下文对象,包含需要解析的源数据
 * @param length 需要解析的文本长度
 * @returns 返回解析得到的原始文本
 */
function parseTextData(context: any, length: number): any {
  console.log("解析 textData");

  // 从 context.source 中截取长度为 length 的文本
  const rawText = context.source.slice(0, length);

  // 根据截取的长度,更新解析位置(光标)
  advanceBy(context, length);

  return rawText;
}

/**
 * 将给定的上下文对象在源代码字符串中向前推进指定数量的字符。
 * @param context {Object} 上下文对象,需要包含一个源代码字符串的属性。
 * @param numberOfCharacters {number} 需要向前推进的字符数量。
 */
function advanceBy(context, numberOfCharacters) {
  console.log("推进代码", context, numberOfCharacters);
  // 更新上下文对象的源代码字符串,使其向前推进指定数量的字符
  context.source = context.source.slice(numberOfCharacters);
}

/**
 * 创建根节点
 * @param children 子节点数组
 * @returns 返回一个对象,表示根节点,包含类型、子节点和辅助函数数组
 */
function createRoot(children) {
  return {
    type: NodeTypes.ROOT, // 根节点的类型
    children, // 子节点数组
    helpers: [], // 辅助函数数组,初始为空
  };
}

这个功能模块的主要作用便是将模板字符串解析为对应的节点信息,并返回上一层。在代码中我们使用了Ast(抽象语法树)中的两个类型,这两个类型的具体实现为以下所示:

export const enum NodeTypes {
  TEXT,
  ROOT,
  INTERPOLATION,
  SIMPLE_EXPRESSION,
  ELEMENT,
  COMPOUND_EXPRESSION
}

export const enum ElementTypes {
  ELEMENT,
}

通过Ast标识符可以帮助编译器更好的理解元素节点代表的含义。