vue3源码解析(二)——Compiler编译器

286 阅读4分钟

Vue.js 3.0的编译器Compiler是将Vue.js的模板编译成渲染函数的核心组件。在这篇文章中,将介绍Vue.js 3.0的编译器的源代码结构和实现原理。

1. 源代码结构

Vue.js 3.0的编译器Compiler的源代码结构包括以下几个文件:

  • compiler-core:包含了编译器的核心代码,包括模板解析、AST构建、代码生成等功能。
  • compiler-dom:包含了编译器生成的DOM渲染函数的相关代码。
  • compiler-sfc:包含了单文件组件(SFC)的编译器相关代码。
  • runtime-core:包含了Vue.js的运行时核心代码,包括响应式、虚拟DOM、组件实例等功能。
  • shared:包含了一些共享的工具函数和数据结构。

2. 实现原理

Vue.js 3.0的编译器Compiler的实现原理包括以下几个步骤:

  • 模板解析:编译器会将模板解析成一棵抽象语法树(AST)。模板解析的过程包括标记化、词法分析和语法分析等步骤。
  • AST转换:编译器会对AST进行一些转换,以优化生成的渲染函数的性能。AST转换的过程包括优化、静态节点提取、事件处理器优化等步骤。
  • 代码生成:编译器会根据AST生成渲染函数的代码。代码生成的过程包括静态节点渲染、动态节点渲染、事件处理器等步骤。
  • 输出结果:编译器会将生成的渲染函数输出为一个JavaScript模块或者一个字符串,以便在Vue.js应用程序中使用。

2.1 模板解析

模板解析是编译器的第一个阶段,其主要任务是将模板字符串解析成抽象语法树(AST)。

// src/compiler-core/src/parse/index.ts

// 模板解析器的入口函数,将模板字符串解析为AST
export function baseParse(content: string, options: ParserOptions = {}): RootNode {
  // 创建解析上下文
  const context = createParserContext(content, options);
  // 解析AST节点
  const root = parseChildren(context, 0 /* TEXT */, []); 
  // ...
  return root;
}

// 解析AST节点
function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors);
  const ns = parent ? parent.ns : Namespaces.HTML;
  const nodes: TemplateChildNode[] = [];

  // 解析模板字符串中的节点,包括标签、文本、注释等
  while (!isEnd(context, mode, ancestors)) {
    const s = context.source;
    let node: TemplateChildNode | undefined = undefined;
    if (mode === TextModes.DATA) {
      // 解析标签、注释节点
      if (startsWith(s, '<')) {
        if (commentRE.test(s)) {
          // 解析注释节点
          node = parseComment(context);
        } else if (conditionalCommentRE.test(s)) {
          // 解析条件注释节点
          node = parseConditionalComment(context);
        } else if (DOCTYPE_RE.test(s)) {
          // 解析Doctype节点
          node = parseBogusComment(context) as any;
        } else {
          // 解析标签节点
          node = parseElement(context, ancestors);
        }
      } else if (s[0] === '{') {
        // 解析插值表达式
        node = parseInterpolation(context, mode);
      }
    }
    // 解析文本节点
    if (!node) {
      node = parseText(context, mode);
    }
    // 将解析出来的节点添加到节点数组中
    if (isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        nodes.push(node[i]);
      }
    } else if (node) {
      if (
        mode !== TextModes.RAWTEXT &&
        (node.type === NodeTypes.TEXT ||
          node.type === NodeTypes.COMMENT ||
          (node.type === NodeTypes.INTERPOLATION && !node.content.trim()))
      ) {
        const prev = last(nodes);
        if (
          prev &&
          prev.type === NodeTypes.TEXT &&
          prev.content[prev.content.length - 1] === ' '
        ) {
          // 合并相邻的文本节点,避免出现空格文本节点
          prev.content += ' ';
          continue;
        }
      }
      nodes.push(node);
    }
  }

  // ...
  return nodes;
}

在解析过程中,编译器会根据当前解析的节点类型,调用相应的解析函数,例如 parseElement 解析标签节点,parseText 解析文本节点等,最终将解析出来的节点添加到节点数组中

2.2 AST转换

在Vue.js 3.0编译器中,AST转换是编译器的第二个阶段,其主要任务是将模板中的静态节点转换成渲染函数中的vnode。

// src/compiler-core/src/transforms/index.ts

// AST转换器的入口函数,将AST转换为渲染函数中的vnode
export function transform(root: RootNode, options: TransformOptions) {
  // ...
  // 进行AST转换
  if (!options.isCustomElement) {
    // 如果不是自定义标签,则进行静态节点提升
    applyTransforms(root, context);
    // ...
  }
  // ...
  return root;
}

// 应用AST转换器
export function applyTransforms(
  root: RootNode,
  context: TransformContext
) {
  // 转换器数组
  const { nodeTransforms } = context;

  // 对AST进行转换,依次调用每个转换器的 transform 函数
  const exitFns = [];
  for (let i = 0; i < nodeTransforms.length; i++) {
    exitFns.push(nodeTransforms[i](root, context));
  }

  // 返回退出函数
  return exitFns;
}

在转换过程中,编译器会依次调用每个AST转换器的 transform 函数,将AST中的节点转换成对应的vnode,最终得到转换后的AST。

例如下面是一个AST转换器的代码示例,用于将静态节点提升为常量:

// src/compiler-core/src/transforms/hoistStatic.ts

// 静态节点提升转换器
export const hoistStaticTransform: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT) {
    // 如果是普通标签,则递归遍历子节点
    let hasHoistedNode = false;
    for (let i = 0; i < node.children.length; i++) {
      const child = node.children[i];
      if (child.type === NodeTypes.ELEMENT) {
        const constantType = doHoist(child, context);
        hasHoistedNode = hasHoistedNode || constantType > 0;
      }
    }
    if (hasHoistedNode) {
      // 如果当前节点的子节点中有被提升的节点,则将当前节点也提升为常量
      node.codegenNode = context.hoist(node.codegenNode!);
    }
  }
};

// 执行静态节点提升
function doHoist(node: TemplateChildNode, context: TransformContext): ConstantTypes {
  if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT) {
    let hasHoistedNode = false;
    // 递归遍历子节点
    for (let i = 0; i < node.children.length; i++) {
      const child = node.children[i];
      if (child.type === NodeTypes.ELEMENT) {
        const constantType = doHoist(child, context);
        hasHoistedNode = hasHoistedNode || constantType > 0;
      } else if (child.type === NodeTypes.TEXT && child.content.trim() === '') {
        // 如果是空文本节点,则移除
        node.children.splice(i, 1);
        i--;
        continue;
      } else if (child.type === NodeTypes.COMMENT) {
        // 如果是注释节点,则移除
        node.children.splice(i, 1);
        i--;
        continue;
      }
      // 如果子节点是静态节点,则进行提升
      if (child.type === NodeTypes.ELEMENT && child.isStatic) {
        hasHoistedNode = true;
        const hoisted = context.hoist(child.codegenNode!);
        child.codegenNode = hoisted;
      }
    }
    // 标记当前节点是否被提升
    if (hasHoistedNode || node.isComponent) {
      return ConstantTypes.HOISTED;
    }
  }
  // 其它节点默认为不可提升
  return ConstantTypes.NOT_CONSTANT;
}

这个示例中,我们定义了一个名为hoistStaticTransform的AST转换器函数,它的作用是将模板中的静态节点提升为常量。

2.3 代码生成

代码生成阶段将主要任务是将经过优化的AST转换成可执行的渲染函数。在Vue.js 3.0编译器中,代码生成是基于模板中的AST节点,通过递归遍历AST树生成目标平台(浏览器、weex等)的代码字符串。

代码生成阶段主要是由codegen函数实现的。这个函数接收一个根节点和一个选项对象作为参数,返回一个对象,其中包含生成的代码和生成的Source Map。

function codegen(root, options) {
  const context = createCodegenContext(options)
  const code = root ? genNode(root, context) : null
  return {
    code: code ? code + `\n` : ``,
    map: context.sourceMap ? context.sourceMap.toJSON() : null
  }
}

在代码生成函数中,我们首先通过调用createCodegenContext函数初始化了一个代码生成上下文。然后,如果根节点存在codegenNode属性,我们会调用genNode函数来生成渲染函数代码,否则返回一个空字符串。

genNode函数中,我们根据节点的类型来生成不同的代码。如果节点是普通字符串,我们直接返回转义后的字符串。如果节点是特殊符号,我们调用genSymbol函数来生成相应的代码。如果节点是虚拟节点,我们调用genElement函数来递归生成子节点的代码。

function genNode(node, context) {
  if (isString(node)) {
    return escapeString(node)
  } else if (isSymbol(node)) {
    return genSymbol(node, context)
  } else {
    return genElement(node, context)
  }
}

genElement函数中,我们先根据节点的属性来生成标签开始部分的代码,包括patchFlag、props、directives和dynamicProps等。然后递归生成子节点的代码,并将它们放在一个_block函数中。最后生成标签结束部分的代码。

function genElement(el, context) {
  const { push, helper } = context
  const { tag, props, children } = el

  push(helper(OPEN_BLOCK))
  push(`'${tag}'`)

  genProps(props, context)
  genChildren(children, context)

  push(helper(CREATE_BLOCK))
}

genProps函数中,我们通过遍历属性数组来生成一个包含所有属性的对象字面量。

function genProps(props, context) {
  const { push } = context
  if (props.length) {
    push(`{`)
    for (let i = 0; i < props.length; i++) {
      const prop = props[i]
      push(`${JSON.stringify(prop.key)}: `)
      push(`${prop.value},`)
    }
    push(`}`)
  }
}

genChildren函数中,我们根据子节点的数量来生成一个包含所有子节点的数组或者单个子节点的表达式。

function genChildren(children, context) {
  const { push } = context
  if (children.length === 1) {
    genNode(children[0], context)
  } else if (children.length > 1) {
    push(`[`)
    for (let i = 0; i < children.length; i++) {
      genNode(children[i], context)
      if (i < children.length - 1) push(`,`)
    }
    push(`]`)
  }
}

2.4 输出结果

输出结果阶段的代码主要是将代码字符串和Source Map对象输出到文件中。

function generate(output, { sourceMap }) {
  const code = output.code + ``
  const map = sourceMap ? output.map + '' : ''

  if (sourceMap) {
    fs.writeFileSync(output.file + '.map', map)
    console.log(`  => Source Map: ${output.file}.map`)
  }

  fs.writeFileSync(output.file, code)
  console.log(`  => Generated: ${output.file}\n`)
}