vue3源码解析:编译之编译器代码生成过程

上文我们讲到了编译时vue如何转换标准AST中vue独有的属性、指令(例如v-for#slot)等内容。本文我们来分析一下转换后的AST最终如何生成代码。

一、示例模板

让我们以一个具体的模板为例,分析代码生成的完整过程:

vue 复制代码
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="handleClick">Count: {{ count }}</button>
  </div>
</template>

二、转换后的 AST 结构

经过解析和转换后,这个模板会生成如下的 AST:

js 复制代码
const ast = {
  type: NodeTypes.ROOT,
  children: [
    {
      type: NodeTypes.ELEMENT,
      tag: "div",
      props: [
        {
          type: NodeTypes.ATTRIBUTE,
          name: "class",
          value: {
            type: NodeTypes.TEXT,
            content: "container",
          },
        },
      ],
      children: [
        {
          type: NodeTypes.ELEMENT,
          tag: "h1",
          children: [
            {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "title",
              },
            },
          ],
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: '"h1"',
            props: null,
            children: {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "title",
              },
            },
            patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
            dynamicProps: null,
            isBlock: false,
            disableTracking: false,
            isComponent: false,
          },
        },
        {
          type: NodeTypes.ELEMENT,
          tag: "button",
          props: [
            {
              type: NodeTypes.DIRECTIVE,
              name: "on",
              arg: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "click",
                isStatic: true,
              },
              exp: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "handleClick",
              },
            },
          ],
          children: [
            {
              type: NodeTypes.TEXT,
              content: "Count: ",
            },
            {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "count",
              },
            },
          ],
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: '"button"',
            props: {
              type: NodeTypes.JS_OBJECT_EXPRESSION,
              properties: [
                {
                  key: "onClick",
                  value: {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content:
                      "_cache[0] || (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args))",
                    isStatic: false,
                  },
                },
              ],
            },
            children: [
              {
                type: NodeTypes.TEXT,
                content: "Count: ",
              },
              {
                type: NodeTypes.INTERPOLATION,
                content: {
                  type: NodeTypes.SIMPLE_EXPRESSION,
                  content: "count",
                },
              },
            ],
            patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
            dynamicProps: null,
            isBlock: false,
            disableTracking: false,
            isComponent: false,
          },
        },
      ],
      codegenNode: {
        type: NodeTypes.VNODE_CALL,
        tag: '"div"',
        props: {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: "class",
              value: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "container",
                isStatic: true,
              },
            },
          ],
        },
        children: [
          // h1 的 codegenNode
          // button 的 codegenNode
        ],
        patchFlag: 0, // 0 表示没有动态内容
        dynamicProps: null,
        isBlock: true,
        disableTracking: false,
        isComponent: false,
      },
    },
  ],
  codegenNode: {
    type: NodeTypes.VNODE_CALL,
    tag: FRAGMENT,
    props: null,
    children: [
      /* div 的 codegenNode */
    ],
    patchFlag: PatchFlags.STABLE_FRAGMENT,
    dynamicProps: null,
    isBlock: true,
    disableTracking: false,
    isComponent: false,
  },
  helpers: new Set([
    CREATE_ELEMENT_VNODE,
    TO_DISPLAY_STRING,
    CREATE_ELEMENT_BLOCK,
    OPEN_BLOCK,
  ]),
  components: [],
  directives: [],
  hoists: [
    {
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          key: "class",
          value: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: "container",
            isStatic: true,
          },
        },
      ],
    },
  ],
  imports: [],
  temps: 0,
  cached: 1,
};

这个 AST 结构展示了转换后的完整状态,包括:

  1. 每个元素节点都添加了 codegenNode,包含生成代码所需的所有信息
  2. 根节点的 codegenNode 使用 Fragment 包装
  3. 静态内容(如 class="container")被提升到 hoists 数组
  4. 事件处理器被缓存(cached 计数为 1)
  5. 添加了所需的辅助函数(helpers)
  6. 为动态内容添加了 patch flags 标记

三、AST 生成的最终代码

让我们看看转换后的 AST 结构生成的最终代码

1. 生成的代码

js 复制代码
// 1. 导入辅助函数
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

// 2. 静态提升的内容
const _hoisted_1 = { class: "container" };

// 3. 渲染函数
export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

2. 代码生成说明

  1. 辅助函数导入

    • createElementVNode:创建普通元素节点
    • toDisplayString:转换插值表达式内容
    • openBlock:开启块级作用域
    • createElementBlock:创建块级元素
  2. 静态提升

    • 将静态的 class 属性提升为常量 _hoisted_1
    • 避免重复创建静态内容
  3. 事件处理

js 复制代码
onClick: _cache[0] ||
  (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args));
  • 使用事件缓存优化
  • 处理可能不存在的方法调用
  1. 动态内容处理
js 复制代码
_toDisplayString(_ctx.title); // 处理 {{ title }}
"Count: " + _toDisplayString(_ctx.count); // 处理 Count: {{ count }}
  1. 补丁标记

    • 1 /* TEXT */:表示节点包含动态文本内容
    • 用于优化更新性能
  2. 块级树优化

    • 使用 _openBlock_createElementBlock 创建块级树
    • 优化更新性能
    • 跟踪动态节点

3. 优化说明

  1. 静态内容优化

    • 静态的 class 属性被提升到渲染函数外部
    • 减少每次渲染时的对象创建
  2. 事件处理优化

    • 使用事件处理器缓存
    • 避免不必要的函数重建
  3. 动态内容标记

    • 使用补丁标记(patch flags)标识动态内容
    • 优化更新性能
  4. 块级树优化

    • 创建块级树结构
    • 精确跟踪动态节点
    • 提高更新性能

这个生成的代码展示了 Vue 编译器的多项优化策略:

  • 静态内容提升
  • 事件处理器缓存
  • 补丁标记
  • 块级树优化
  • 动态节点追踪

四、举例说明生成代码的详细过程

让我们按照 generate 函数的执行流程,详细分析代码生成的每个步骤:

4.1 创建上下文和初始设置

源码分析

js// 复制代码
const context = createCodegenContext(ast, options);
if (options.onContextCreated) options.onContextCreated(context);

// 2. 从上下文中解构需要的工具函数和状态
const {
  mode, // 编译模式:module/function
  push, // 用于向代码字符串追加内容
  prefixIdentifiers, // 是否添加标识符前缀
  indent, // 增加缩进级别
  deindent, // 减少缩进级别
  newline, // 插入换行
  scopeId, // 作用域ID
  ssr, // 是否服务端渲染
} = context;

// 3. 获取辅助函数信息
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== "module";

context 对象变化

js 复制代码
// 初始状态
context = {
  mode: "module",
  code: "",
  column: 1,
  line: 1,
  offset: 0,
  indentLevel: 0,
  pure: false,
  map: undefined,
  source: ast.source,
  // ... 其他属性
};

// helpers 数组内容(基于我们的示例)
helpers = [
  CREATE_ELEMENT_VNODE,
  TO_DISPLAY_STRING,
  OPEN_BLOCK,
  CREATE_ELEMENT_BLOCK,
];

4.2 生成前导代码

源码分析

js 复制代码
// 1. 创建前导代码上下文
const preambleContext = isSetupInlined
  ? createCodegenContext(ast, options)
  : context;

// 2. 根据模式生成不同的前导代码
if (!__BROWSER__ && mode === "module") {
  genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined);
} else {
  genFunctionPreamble(ast, preambleContext);
}

模块模式下的前导代码生成

js 复制代码
function genModulePreamble(ast, context, genScopeId, inline) {
  const { push, newline, runtimeModuleName } = context;

  // 1. 生成辅助函数导入
  if (ast.helpers.size) {
    push(
      `import { ${helpers
        .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(", ")} } from ${JSON.stringify(runtimeModuleName)}\n`
    );
  }

  // 2. 生成静态提升的变量
  genHoists(ast.hoists, context);
  newline();

  // 3. 添加 export 关键字
  if (!inline) {
    push(`export `);
  }
}

代码生成过程

js 复制代码
// context.code 的变化
context.code = ''
↓
// 添加导入语句
context.code = `import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock
} from "vue"\n`
↓
// 添加静态提升的内容
context.code += `\nconst _hoisted_1 = { class: "container" }\n`
↓
// 添加 export 关键字
context.code += `\nexport `

4.3 生成渲染函数签名

源码分析

js 复制代码
// 1. 确定函数名和参数
const functionName = ssr ? `ssrRender` : `render`;
const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];

// 2. 生成函数声明
if (isSetupInlined) {
  push(`(${signature}) => {`);
} else {
  push(`function ${functionName}(${args.join(", ")}) {`);
}
indent();

// 3. 处理 with 块(如果需要)
if (useWithBlock) {
  push(`with (_ctx) {`);
  indent();
  // 在 with 块内声明辅助函数
  if (hasHelpers) {
    push(
      `const { ${helpers.map(aliasHelper).join(", ")} } = _Vue\n`,
      NewlineType.End
    );
    newline();
  }
}

代码生成过程

js 复制代码
// context.code 的变化
context.code += `function render(_ctx, _cache) {`
↓
context.code += `\n  `  // 缩进增加

4.4 生成 VNode 树

源码分析

js 复制代码
// 1. 生成返回语句
if (!ssr) {
  push(`return `);
}

// 2. 生成根节点
if (ast.codegenNode) {
  genNode(ast.codegenNode, context);
} else {
  push(`null`);
}

节点生成的核心函数

js 复制代码
// genNode 是代码生成的核心函数,负责处理所有类型的节点
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  // 1. 处理字符串节点
  if (isString(node)) {
    context.push(node, NewlineType.Unknown);
    return;
  }
  // 2. 处理符号节点(通常是辅助函数)
  if (isSymbol(node)) {
    context.push(context.helper(node));
    return;
  }

  // 3. 根据节点类型分发到不同的处理函数
  switch (node.type) {
    // 3.1 元素、条件和循环节点
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      // 确保节点已经过转换
      genNode(node.codegenNode!, context);
      break;

    // 3.2 文本节点
    case NodeTypes.TEXT:
      genText(node, context);
      break;

    // 3.3 简单表达式
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context);
      break;

    // 3.4 插值表达式 {{ xxx }}
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;

    // 3.5 虚拟节点调用
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;

    // 3.6 复合表达式
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context);
      break;

    // ... 其他类型的处理
  }
}

// 处理文本节点
function genText(node: TextNode, context: CodegenContext) {
  context.push(JSON.stringify(node.content), NewlineType.Unknown, node);
}

// 处理表达式节点
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
  const { content, isStatic } = node;
  context.push(
    isStatic ? JSON.stringify(content) : content,
    NewlineType.Unknown,
    node
  );
}

// 处理插值表达式
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
  const { push, helper, pure } = context;
  if (pure) push(PURE_ANNOTATION);
  push(`${helper(TO_DISPLAY_STRING)}(`);
  genNode(node.content, context);
  push(`)`);
}

// 处理虚拟节点调用
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure } = context;
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock,
    disableTracking,
    isComponent,
  } = node;

  // 1. 处理指令
  if (directives) {
    push(helper(WITH_DIRECTIVES) + `(`);
  }

  // 2. 处理块级节点
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `);
  }

  // 3. 添加纯注释标记
  if (pure) {
    push(PURE_ANNOTATION);
  }

  // 4. 选择创建节点的辅助函数
  const callHelper = isBlock
    ? getVNodeBlockHelper(context.inSSR, isComponent)
    : getVNodeHelper(context.inSSR, isComponent);

  push(helper(callHelper) + `(`, NewlineType.None, node);

  // 5. 生成参数列表
  genNodeList(
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  );

  push(`)`);

  // 6. 处理块级和指令的结束
  if (isBlock) {
    push(`)`);
  }
  if (directives) {
    push(`, `);
    genNode(directives, context);
    push(`)`);
  }
}

// 处理节点列表生成
function genNodeList(
  nodes: (string | symbol | CodegenNode)[],
  context: CodegenContext
) {
  const { push, newline } = context;

  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (isString(node)) {
      push(node);
    } else if (isSymbol(node)) {
      push(context.helper(node));
    } else {
      genNode(node, context);
    }
    if (i < nodes.length - 1) {
      push(", ");
    }
  }
}

// 处理可能为空的参数列表
function genNullableArgs(args: any[]): any[] {
  // 从后往前寻找第一个非空参数
  let i = args.length;
  while (i--) {
    if (args[i] != null) break;
  }
  // 返回截至最后一个非空参数的列表
  return args.slice(0, i + 1);
}

代码生成过程

js 复制代码
// 1. 初始状态
context = {
  code: `function render(_ctx, _cache) {\n  return `,
  indentLevel: 1,
  line: 2,
  column: 9,
};

// 2. 生成根节点开始
context.code += `(_openBlock(), _createElementBlock("div", _hoisted_1, [`;
context.column += 52;

// 3. 生成 h1 节点
context.code += `\n  _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),`;
context.line += 1;
context.column = 78;

// 4. 生成 button 节点
context.code += `\n  _createElementVNode("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
  }, "Count: " + _toDisplayString(_ctx.count), 1 /* TEXT */)`;
context.line += 3;
context.column = 63;

// 5. 完成节点生成
context.code += `\n]))`;
context.line += 1;
context.column = 4;

在这个过程中,genNode 函数通过递归遍历 AST 节点树,为每种类型的节点生成相应的代码:

  1. 元素节点 :生成 createElementVNodecreateElementBlock 调用
  2. 文本节点:直接生成字符串字面量
  3. 插值表达式 :生成 toDisplayString 调用
  4. 指令:生成相应的指令处理代码
  5. 事件处理器:生成带缓存的事件处理函数

每个节点的生成都会考虑:

  • 静态/动态内容的处理
  • 优化标记的添加
  • 缓存策略的应用
  • 块级树的构建

4.5 生成函数结束

源码分析

js 复制代码
// 1. 处理 with 块结束
if (useWithBlock) {
  deindent();
  push(`}`);
}

// 2. 结束渲染函数
deindent();
push(`}`);

// 3. 返回生成结果
return {
  ast,
  code: context.code,
  preamble: isSetupInlined ? preambleContext.code : ``,
  map: context.map ? context.map.toJSON() : undefined,
};

最终生成的完整代码

js 复制代码
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

const _hoisted_1 = { class: "container" };

export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

下面通过一个简单的流程图将整个代码生成过程过一遍。

js 复制代码
模板 Template
    ↓
解析 Parse
    ↓
AST 生成
    ↓
转换 Transform
    ↓
代码生成 CodeGen
    |
    ├── 1. 创建上下文
    |     ├── 初始化 context 对象
    |     └── 收集 helpers
    |
    ├── 2. 生成前导代码
    |     ├── 导入辅助函数
    |     ├── 静态提升
    |     └── 导出声明
    |
    ├── 3. 生成渲染函数
    |     ├── 函数签名
    |     └── with 块处理
    |
    ├── 4. 生成 VNode 树
    |     ├── genNode
    |     |    ├── 文本节点
    |     |    ├── 表达式节点
    |     |    ├── 插值节点
    |     |    └── VNode 调用
    |     |
    |     └── 优化处理
    |          ├── 静态提升
    |          ├── 补丁标记
    |          ├── 块级树
    |          └── 事件缓存
    |
    └── 5. 完成生成
          ├── 闭合函数
          └── 返回结果

五、优化说明

让我们基于前面的示例模板来分析各项优化策略:

vue 复制代码
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="handleClick">Count: {{ count }}</button>
  </div>
</template>

1. 静态提升 (Static Hoisting)

在我们的示例中,静态的 class 属性被提升:

js 复制代码
// 静态提升的内容
const _hoisted_1 = { class: "container" };

// 渲染函数中直接使用提升的内容
return _createElementBlock("div", _hoisted_1, [
  // ... 子节点
]);

这种优化的效果:

  • 将静态的 class="container" 提升到渲染函数外部
  • 避免在每次渲染时重新创建对象
  • 减少内存分配和垃圾回收的开销

2. 动态内容处理 (Dynamic Content)

示例中有两处动态内容:

js 复制代码
// 1. h1 中的动态文本
_createElementVNode(
  "h1",
  null,
  _toDisplayString(_ctx.title), // 动态内容
  1 /* TEXT */  // 补丁标记
);

// 2. button 中的动态文本
_createElementVNode(
  "button",
  { onClick: /*...*/ },
  "Count: " + _toDisplayString(_ctx.count), // 动态内容
  1 /* TEXT */  // 补丁标记
);

优化效果:

  • 使用 _toDisplayString 处理动态文本
  • 通过补丁标记 1 /* TEXT */ 标识动态内容
  • 更新时只处理文本部分,不影响节点本身

3. 事件处理优化 (Event Handling)

示例中的点击事件处理:

js 复制代码
_createElementVNode(
  "button",
  {
    onClick:
      _cache[0] ||
      (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args)),
  }
  // ... 内容
);

优化效果:

  • 使用 _cache[0] 缓存事件处理函数
  • 避免每次渲染时重新创建函数
  • 处理方法可能不存在的情况 (_ctx.handleClick && _ctx.handleClick)

4. 块级树优化 (Block Tree)

示例中的块级树结构:

js 复制代码
// 1. 开启块级作用域
_openBlock();

// 2. 创建块级元素
_createElementBlock("div", _hoisted_1, [
  // 子节点被收集到 block 的 dynamicChildren 中
  _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1),
  _createElementVNode("button", /*...*/, /*...*/, 1)
]);

优化效果:

  • 根节点 div 被标记为块级节点
  • 动态子节点 (h1 和 button) 被收集到 dynamicChildren 数组
  • 更新时可以直接定位到动态节点,跳过静态内容

5. 优化策略的综合应用

让我们看看这些优化策略如何在示例中协同工作:

js 复制代码
import /*...*/ "vue";

// 1. 静态提升
const _hoisted_1 = { class: "container" };

// 2. 渲染函数
export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      // 动态文本节点
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      // 带缓存的事件处理器和动态文本
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

这个示例展示了优化策略的综合效果:

  • 静态的 class 属性被提升
  • 动态文本内容使用补丁标记
  • 事件处理器被缓存
  • 使用块级树结构追踪动态节点
  • 生成的代码既简洁又高效

六、总结

代码生成是vue编译过程的最后一步,它将优化后的 AST 转换为可执行的渲染函数,在这个过程中,编译器实现了多种优化策略,如静态提升、动态内容标记、事件缓存、块树优化。至此,我们完成了整个编译流程的分析。

相关推荐
素界UI设计1 小时前
开源网页生态掘金:从Bootstrap二次开发到行业专属组件库的技术变现
前端·开源·bootstrap
潘小安1 小时前
【译】六个开发高手使用的 css 动画秘诀
前端·css·性能优化
前端开发爱好者1 小时前
尤雨溪官宣:Vite 历史性的一刻!超越 Webpack!
前端·javascript·vite
前端开发爱好者1 小时前
Vue3 "抛弃" Axios !用上了 专属请求库!
前端·javascript·vue.js
前端开发爱好者1 小时前
"Lodash" 的终极版!Vue、React 通杀!
前端·javascript·全栈
前端开发爱好者1 小时前
TanStack:不止于 Vue!一个库,真·通杀所有框架!
前端·javascript·vue.js
curdcv_po1 小时前
Three.js,给纹理,设颜色空间
前端
站大爷IP1 小时前
HTTPS代理抓包完全攻略:工具、配置与高级技巧
前端
洛卡卡了1 小时前
“改个配置还要发版?”搞个配置后台不好吗
前端·后端·架构
林太白2 小时前
CommonJS和ES Modules篇
前端·面试