Vue3核心源码解析第十一课 AST生成代码

发布时间:2022-11-09 21:30

16 生成代码:AST 如何生成可运行的代码?(上)

上一节课我们分析了 AST 节点转换的过程,也知道了 AST 节点转换的作用是通过语法分析,创建了语义和信息更加丰富的代码生成节点 codegenNode,便于后续生成代码。

那么这一节课,我们就来分析整个编译的过程的最后一步——代码生成的实现原理。

同样的,代码生成阶段由于要处理的场景很多,所以代码也非常多而复杂。为了方便你理解它的核心流程,我们还是通过这个示例来演示整个代码生成的过程:

class="app"> <hello v-if="flag">hello> <div v-else> <p>hello {{ msg + test }}p> <p>staticp> <p>staticp> div>

代码生成的结果是和编译配置相关的,你可以打开官方提供的模板导出工具平台,点击右上角的 Options 修改编译配置。为了让你理解核心的流程,这里我只分析一种配置方案,当然当你理解整个编译核心流程后,也可以修改这些配置分析其他的分支逻辑。

我们分析的编译配置是:mode 为 module,prefixIdentifiers 开启,hoistStatic 开启,其他配置均不开启。

为了让你有个大致印象,我们先来看一下上述例子生成代码的结果:

import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = { class: "app" }
const _hoisted_2 = { key: 1 }
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
export function render(_ctx, _cache) {
  const _component_hello = _resolveComponent("hello")
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    (_ctx.flag)
      ? _createVNode(_component_hello, { key: 0 })
      : (_openBlock(), _createBlock("div", _hoisted_2, [
          _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 /* TEXT */),
          _hoisted_3,
          _hoisted_4
        ]))
  ]))
}

示例的模板是如何转换生成这样的代码的?在 AST 转换后,会执行 generate 函数生成代码:

return generate(ast, extend({}, options, {
  prefixIdentifiers
}))

generate 函数的输入就是转换后的 AST 根节点,我们看一下它的实现:

function generate(ast, options = {}) {
  // 创建代码生成上下文
  const context = createCodegenContext(ast, options);
  const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context;
  const hasHelpers = ast.helpers.length > 0;
  const useWithBlock = !prefixIdentifiers && mode !== 'module';
  const genScopeId = scopeId != null && mode === 'module';
  // 生成预设代码
  if ( mode === 'module') {
    genModulePreamble(ast, context, genScopeId);
  }
  else {
    genFunctionPreamble(ast, context);
  }
  if (!ssr) {
    push(`function render(_ctx, _cache) {`);
  }
  else {
    push(`function ssrRender(_ctx, _push, _parent, _attrs) {`);
  }
  indent();
  if (useWithBlock) {
    // 处理带 with 的情况,Web 端运行时编译
    push(`with (_ctx) {`);
    indent();
    if (hasHelpers) {
      push(`const { ${ast.helpers
        .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
        .join(', ')} } = _Vue`);
      push(`\n`);
      newline();
    }
  }
  // 生成自定义组件声明代码
  if (ast.components.length) {
    genAssets(ast.components, 'component', context);
    if (ast.directives.length || ast.temps > 0) {
      newline();
    }
  }
  // 生成自定义指令声明代码
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context);
    if (ast.temps > 0) {
      newline();
    }
  }
  // 生成临时变量代码
  if (ast.temps > 0) {
    push(`let `);
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`);
    }
  }
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`);
    newline();
  }
  if (!ssr) {
    push(`return `);
  }
  // 生成创建 VNode 树的表达式
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context);
  }
  else {
    push(`null`);
  }
  if (useWithBlock) {
    deindent();
    push(`}`);
  }
  deindent();
  push(`}`);
  return {
    ast,
    code: context.code,
    map: context.map ? context.map.toJSON() : undefined
  };
}

generate 主要做五件事情:创建代码生成上下文,生成预设代码,生成渲染函数,生成资源声明代码,以及生成创建 VNode 树的表达式。接下来,我们就依次详细分析这几个流程。

创建代码生成上下文

首先,是通过执行 createCodegenContext 创建代码生成上下文,我们来看它的实现:

function createCodegenContext(ast, { mode = 'function', prefixIdentifiers = mode === 'module', sourceMap = false, filename = `template.vue.html`, scopeId = null, optimizeBindings = false, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, ssr = false }) {
  const context = {
    mode,
    prefixIdentifiers,
    sourceMap,
    filename,
    scopeId,
    optimizeBindings,
    runtimeGlobalName,
    runtimeModuleName,
    ssr,
    source: ast.loc.source,
    code: ``,
    column: 1,
    line: 1,
    offset: 0,
    indentLevel: 0,
    pure: false,
    map: undefined,
    helper(key) {
      return `_${helperNameMap[key]}`
    },
    push(code) {
      context.code += code
    },
    indent() {
      newline(++context.indentLevel)
    },
    deindent(withoutNewLine = false) {
      if (withoutNewLine) {
        --context.indentLevel
      }
      else {
        newline(--context.indentLevel)
      }
    },
    newline() {
      newline(context.indentLevel)
    }
  }
  function newline(n) {
    context.push('\n' + `  `.repeat(n))
  }
  return context
}

这个上下文对象 context 维护了 generate 过程的一些配置,比如 mode、prefixIdentifiers;也维护了 generate 过程的一些状态数据,比如当前生成的代码 code,当前生成代码的缩进 indentLevel 等。

此外,context 还包含了在 generate 过程中可能会调用的一些辅助函数,接下来我会介绍几个常用的方法,它们会在整个代码生成节点过程中经常被用到。

  • push(code),就是在当前的代码 context.code 后追加 code 来更新它的值。

  • indent(),它的作用就是增加代码的缩进,它会让上下文维护的代码缩进 context.indentLevel 加 1,内部会执行 newline 方法,添加一个换行符,以及两倍indentLevel 对应的空格来表示缩进的长度。

  • deindent(),和 indent 相反,它会减少代码的缩进,让上下文维护的代码缩进 context.indentLevel 减 1,在内部会执行 newline 方法去添加一个换行符,并减少两倍indentLevel 对应的空格的缩进长度。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号