深入理解 JavaScript 模块系统:CJS 与 ESM 的实现原理

你真的理解 requireimport 的区别吗?不只是语法不同,它们的加载时机、值的传递方式、循环依赖处理都截然不同。本文通过 require 源码和 ESM 规范,解释这些差异背后的实现机制,让你彻底搞懂 JavaScript 模块系统的运行原理。

模块化的演进

JavaScript 最初为浏览器脚本语言,没有模块系统。随着应用规模增长,全局变量污染和代码组织问题凸显,催生了模块化方案。

全局变量时代:所有代码共享全局作用域,变量冲突频发。

javascript 复制代码
// 多个脚本容易产生命名冲突
var data = "script1";
var data = "script2"; // 覆盖前一个

IIFE 模式:利用函数作用域隔离变量。

javascript 复制代码
var Module = (function () {
  var private = "private";
  return { public: "public" };
})();

CJS (2009):Node.js 采用,服务端模块规范。

AMD (2010):RequireJS 推广,浏览器异步加载方案。

ESM(2015):ECMAScript 官方标准,静态结构。

语法对比

CJS 语法

导出方式

javascript 复制代码
// 1. module.exports 导出对象
module.exports = { name: "foo", version: "1.0" };

// 2. module.exports 导出函数
module.exports = function () {};

// 3. module.exports 导出类
module.exports = class MyClass {};

// 4. exports 添加属性(exports 是 module.exports 的引用)
exports.name = "foo";
exports.version = "1.0";

// 注意:直接赋值 exports 会断开引用
// exports = {} // ❌ 无效

导入方式

javascript 复制代码
// 1. 导入整个模块
const module = require("./module");

// 2. 解构导入
const { name, version } = require("./module");

// 3. 动态路径(运行时计算)
const env = "production";
const config = require(`./config.${env}`);

// 4. 条件导入
if (condition) {
  const module = require("./module");
}

ESM 语法

导出方式

javascript 复制代码
// 1. 命名导出 (Named Export)
export const name = 'foo';
export function fn() {}
export class MyClass {}

// 2. 批量命名导出
const name = 'foo';
const version = '1.0';
export { name, version };

// 3. 重命名导出
export { name as moduleName };

// 4. 默认导出 (Default Export) - 每个模块只能有一个
export default function() {}
// 或
export default class MyClass {}
// 或
export default { name: 'foo' };

// 5. 混合导出(命名 + 默认)
export const name = 'foo';
export default function() {}

// 6. 转发导出
export { name } from './other.js';
export * from './other.js';
export { default as otherDefault } from './other.js';

导入方式

javascript 复制代码
// 1. 导入命名导出
import { name, version } from "./module.js";

// 2. 导入并重命名
import { name as moduleName } from "./module.js";

// 3. 导入默认导出
import MyModule from "./module.js";

// 4. 混合导入
import MyModule, { name, version } from "./module.js";

// 5. 导入所有命名导出为命名空间对象
import * as Module from "./module.js";

// 6. 仅执行模块
import "./module.js";

// 7. 动态导入(可在任意位置,返回 Promise)
const module = await import("./module.js");
// 或
if (condition) {
  import("./module.js").then((module) => {});
}

CJS 加载机制

CJS 是 Node.js 实现的模块系统。不同于语言层面的特性,CJS 是一个运行时概念 ,核心是 Node.js 内置的 require 函数。理解 require 的实现原理,有助于理解 CJS 的各种特性。

require 实现原理

require 函数负责加载模块,内部维护 require.cache 缓存对象。下面是简化的伪源码:

javascript 复制代码
function require(modulePath) {
  // 1. 解析为绝对路径
  const absolutePath = require.resolve(modulePath);

  // 2. 检查缓存
  if (require.cache[absolutePath]) {
    return require.cache[absolutePath].exports;
  }

  // 3. 创建 Module 对象
  const module = {
    id: absolutePath,
    exports: {},
    loaded: false,
    // ... 其他属性
  };

  // 4. 提前放入缓存(处理循环依赖的关键)
  require.cache[absolutePath] = module;

  // 5. 读取文件内容
  const code = fs.readFileSync(absolutePath, "utf8");

  // 6. 包装为函数
  const wrapper = "(function (exports, require, module, __filename, __dirname) { " + code + "\n});";

  // 7. 编译并执行
  const compiledWrapper = vm.runInThisContext(wrapper);
  compiledWrapper.call(
    module.exports, // this 指向 exports
    module.exports, // 参数 1: exports
    require, // 参数 2: require
    module, // 参数 3: module
    absolutePath, // 参数 4: __filename
    path.dirname(absolutePath) // 参数 5: __dirname
  );

  // 8. 标记为已加载
  module.loaded = true;

  // 9. 返回 module.exports
  return module.exports;
}

// require.cache: 缓存对象
require.cache = {};

// require.resolve: 解析路径
require.resolve = function (modulePath) {
  // 解析算法:相对路径、绝对路径、node_modules 查找等
  return absolutePath;
};

从伪源码可以看出,require 做了这些事:解析路径、检查缓存、创建 Module 对象、提前放入缓存、包装并执行代码、返回 module.exports。这种设计带来了以下特性。

官方文档Node.js Modules: The module wrapper | Node.js Modules: require

同步执行,不支持 top-level await

从伪源码的第 5 步可以看到,require 使用 fs.readFileSync 同步读取文件,会阻塞代码直到模块加载完成。由于 CJS 模块代码被包装在普通函数中(非 async 函数),因此不支持 top-level await。

javascript 复制代码
// module.js
console.log("module 执行");
module.exports = { name: "foo" };

// main.js
console.log("开始");
const mod = require("./module"); // 阻塞,等待 module.js 执行完成
console.log("结束", mod.name);

// 输出:
// 开始
// module 执行
// 结束 foo

// ❌ CJS 不支持 top-level await
// await someAsyncFunction(); // SyntaxError: await is only valid in async functions

值拷贝

伪源码的第 4 步将 Module 对象放入缓存,第 2 步检查缓存时直接返回 module.exports。这意味着所有 require 返回的是同一个对象引用。

javascript 复制代码
// counter.js
let count = 0;
module.exports = {
  count: count, // 导出时 count 的值为 0
  increment() {
    count++;
  },
};

// main.js
const counter = require("./counter");
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0(对象属性未变,count 是模块内部变量)

// other.js
const counter2 = require("./counter");
console.log(counter === counter2); // true(同一个对象)

循环依赖处理

伪源码的关键设计是第 4 步:在模块代码执行前,就将 Module 对象放入缓存 (此时 loaded: false)。这使得循环依赖时,后加载的模块能拿到前一个模块未完成的 exports

javascript 复制代码
// a.js
console.log("a 开始");
exports.done = false;
const b = require("./b");
console.log("在 a 中,b.done =", b.done);
exports.done = true;
console.log("a 结束");

// b.js
console.log("b 开始");
exports.done = false;
const a = require("./a"); // 此时 a 未执行完,exports.done = false
console.log("在 b 中,a.done =", a.done);
exports.done = true;
console.log("b 结束");

// main.js
require("./a");

// 输出:
// a 开始
// b 开始
// 在 b 中,a.done = false
// b 结束
// 在 a 中,b.done = true
// a 结束

官方文档Node.js Modules: Cycles

不要重新赋值 exports

伪源码第 7 步执行包装函数时,将 module.exports 作为参数传入,赋值给 exports。这意味着 exports 只是 module.exports 的引用。一旦重新赋值 module.exports,引用关系就断开,之后修改 exports 就无法导出了。

javascript 复制代码
// module.js
exports.name = "foo";
console.log(exports === module.exports); // true

module.exports = { version: "1.0" }; // 重新赋值
console.log(exports === module.exports); // false

exports.age = 18; // 无效,exports 已失效

// main.js
const mod = require("./module");
console.log(mod); // { version: '1.0' }

缓存清除与模块重载

由于 require.cache 是一个普通的 JavaScript 对象,可以通过删除缓存来强制重新加载模块。这在开发热重载等场景中很有用。

javascript 复制代码
// module.js
console.log("模块执行");
module.exports = { timestamp: Date.now() };

// main.js
const mod1 = require("./module"); // 输出: 模块执行
console.log(mod1.timestamp);

// 删除缓存
delete require.cache[require.resolve("./module")];

const mod2 = require("./module"); // 再次输出: 模块执行
console.log(mod2.timestamp); // 不同的时间戳
console.log(mod1 === mod2); // false

官方文档Node.js Modules: require.cache

ESM 加载机制

ESM 是 ECMAScript 标准定义的模块系统,是语言层面的特性。不同于 CJS 的运行时加载,ESM 在代码执行前进行静态分析,确定模块依赖关系。

ESM 加载过程

ESM 的加载过程是将入口文件(entry point file)转换为模块实例(module instance)的过程。这个过程分为三个阶段:Construction (构建)、Instantiation (实例化)、Evaluation(求值)。

核心概念

Module Record(模块记录)

  • 文件被解析后生成的数据结构
  • 包含模块的 import/export 信息、代码等

Module Instance(模块实例)

  • 由代码(code)和状态(state)组成
  • 代码是指令集(如何做事的配方)
  • 状态是变量的实际值(存储在内存中)

Entry Point File(入口文件)

  • 模块图的起点
  • 浏览器通过 <script type="module" src="main.js"> 指定入口

阶段 1:Construction(构建)

查找、获取、解析文件,构建模块图。

graph TD A[入口文件 main.js] --> B[解析 import 语句] B --> C[找到依赖 counter.js] C --> D[获取 counter.js] D --> E[解析 counter.js] E --> F[递归处理所有依赖] F --> G[生成 Module Records]

关键点

  • Loader 负责查找和获取文件,浏览器和 Node 的 loader 不同
  • 解析时识别所有静态 import/export 声明
  • 逐层构建完整的模块依赖图
  • 动态 import() 不在此阶段处理

Module Map(模块映射)

  • Loader 使用 Module Map 缓存模块
  • 键是模块的唯一标识,值是 Module Record
  • 确保每个模块只被加载和解析一次
javascript 复制代码
// Module Map 示例(概念)
{
  'https://example.com/main.js': ModuleRecord { ... },
  'https://example.com/counter.js': ModuleRecord { ... }
}

阶段 2:Instantiation(实例化)

在内存中为导出值分配空间,建立 import/export 的实时绑定(live bindings)。

graph TD A[遍历模块图] --> B[创建 Module Environment Record] B --> C[为 export 在内存中分配空间] C --> D[建立 import/export 的实时绑定] D --> E[验证所有 import 有对应的 export]

实时绑定(Live Bindings)

  • Export 和 import 指向内存中的同一个位置
  • 导出模块修改值,导入模块能看到变化
  • 与 CJS 的值拷贝不同
javascript 复制代码
// counter.js
export let count = 0;
export function increment() {
  count++; // 修改 count
}

// main.js
import { count, increment } from "./counter.js";
console.log(count); // 0
increment();
console.log(count); // 1(实时绑定,看到了变化)

关键点

  • 此阶段只分配内存,不填充值
  • 导出的函数声明会在此阶段初始化
  • 使用深度优先后序遍历(depth-first post-order traversal)

阶段 3:Evaluation(求值)

执行模块的顶层代码(top-level code),填充内存中的值。

graph TD A[按依赖顺序执行模块] --> B[执行顶层代码] B --> C[填充导出值] C --> D{有副作用?} D -->|是| E[触发副作用
如网络请求] D -->|否| F[完成] E --> F

关键点

  • 顶层代码:函数外的代码
  • 每个模块只求值一次(Module Map 确保)
  • 可能产生副作用(side effects):网络请求、修改 DOM 等
  • 深度优先后序遍历:先求值依赖,再求值当前模块

浏览器和 Node.js 的 Construction 差异

ESM 的三阶段加载流程在浏览器和 Node.js 中是一致的,但在 Construction 阶段存在差异。Construction 包含两个过程:Module Resolution (模块解析,确定模块路径)和 Fetch(获取文件)。

浏览器

Module Resolution(模块解析):

浏览器使用完整的 URL 作为模块标识符。

html 复制代码
<!-- 入口文件 -->
<script type="module" src="/main.js"></script>
javascript 复制代码
// main.js - 必须使用相对路径或绝对路径
import { add } from "./math.js"; // ✅ 相对路径
import { config } from "/config.js"; // ✅ 绝对路径
import { api } from "https://cdn.example.com/api.js"; // ✅ 完整 URL

// ❌ 裸模块标识符(bare specifier)不支持
import { lodash } from "lodash"; // 报错:Failed to resolve module specifier

Import Maps:浏览器通过 Import Maps 支持裸模块标识符。

html 复制代码
<script type="importmap">
  {
    "imports": {
      "lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.js",
      "vue": "/node_modules/vue/dist/vue.esm-browser.js"
    }
  }
</script>

<script type="module">
  import _ from "lodash"; // ✅ 解析为 https://cdn.jsdelivr.net/npm/...
  import { createApp } from "vue"; // ✅ 解析为 /node_modules/vue/...
</script>

官方文档HTML Standard: Import maps

Fetch(获取文件):

  • 并行下载:浏览器会并行发起多个 HTTP 请求下载依赖模块
  • 阻塞行为:必须等所有依赖下载完才能进入 Instantiation 阶段
  • 非阻塞渲染<script type="module"> 默认具有 defer 特性,不会阻塞页面渲染
graph TD A[Module Resolution:
解析模块路径] --> B[Fetch: 发起 HTTP 请求] B --> C[并行下载 a.js] B --> D[并行下载 b.js] B --> E[并行下载 c.js] C --> F[等待所有依赖下载完成] D --> F E --> F F --> G[进入 Instantiation 阶段]

Node.js

Module Resolution(模块解析):

Node.js 支持裸模块标识符,使用复杂的解析算法查找模块。

javascript 复制代码
// Node.js 支持多种导入方式
import { readFile } from "fs"; // ✅ 内置模块
import express from "express"; // ✅ node_modules 查找
import { add } from "./math.js"; // ✅ 相对路径
import { config } from "/abs/path/config.js"; // ✅ 绝对路径

Node.js 的解析算法:

  1. 内置模块 :如 fspath,直接返回
  2. 相对/绝对路径:按路径查找
  3. 裸模块标识符 :从当前目录开始,逐层向上查找 node_modules
javascript 复制代码
// 当前文件:/project/src/index.js
import express from "express";

// Node.js 查找顺序:
// 1. /project/src/node_modules/express
// 2. /project/node_modules/express
// 3. /node_modules/express

package.json 的 exports 字段

json 复制代码
{
  "name": "my-package",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}
javascript 复制代码
import pkg from "my-package"; // 解析为 ./dist/index.js
import utils from "my-package/utils"; // 解析为 ./dist/utils.js

官方文档Node.js: Modules: Packages | Node.js: ECMAScript modules

Fetch(获取文件):

  • 同步读取:Node.js 使用同步 I/O 读取本地文件
  • 无网络延迟:读取本地文件速度快,但仍需等待所有依赖读取完
  • 性能影响:大量模块时文件 I/O 仍有性能影响
graph TD A[Module Resolution:
解析模块路径] --> B[Fetch: 同步读取 a.js] B --> C[同步读取 b.js] C --> D[同步读取 c.js] D --> E[所有文件读取完成] E --> F[进入 Instantiation 阶段]

动态 import

ESM 提供两种导入方式:静态导入import 声明)和动态导入import() 函数)。

静态导入

静态导入在 Construction 阶段处理,必须在模块顶层使用。

javascript 复制代码
// ✅ 顶层静态导入
import { add } from "./math.js";
import express from "express";

// ❌ 不能在代码块、函数、条件语句中使用
if (condition) {
  import { add } from "./math.js"; // SyntaxError
}

function loadModule() {
  import express from "express"; // SyntaxError
}

特点

  • 在代码执行前完成(Construction 阶段)
  • 支持静态分析:打包工具可进行 tree-shaking
  • 路径必须是静态字符串(不能是变量)
javascript 复制代码
const path = './math.js';
import { add } from path;  // ❌ SyntaxError

动态导入 import()

import() 是一个返回 Promise 的函数,可在任意位置使用。

javascript 复制代码
// ✅ 条件加载
if (condition) {
  const module = await import("./module.js");
}

// ✅ 函数内使用
async function loadModule() {
  const express = await import("express");
  return express.default;
}

// ✅ 动态路径
const env = "production";
const config = await import(`./config.${env}.js`);

// ✅ 按需加载(代码分割)
button.addEventListener("click", async () => {
  const { Chart } = await import("./chart.js");
  new Chart(data);
});

返回值:Module Namespace Object(模块命名空间对象)

javascript 复制代码
// math.js
export const add = (a, b) => a + b;
export default function multiply(a, b) {
  return a * b;
}

// main.js
const module = await import("./math.js");
console.log(module);
// {
//   add: [Function: add],
//   default: [Function: multiply]
// }

module.add(1, 2); // 3
module.default(2, 3); // 6

静态导入 vs 动态导入

特性 静态导入 import 动态导入 import()
语法 声明语句 函数调用(返回 Promise)
使用位置 仅模块顶层 任意位置(函数、代码块等)
路径 必须是静态字符串 可以是动态表达式
执行时机 Construction 阶段 运行时(Evaluation 阶段)
Tree-shaking ✅ 支持 ❌ 不支持
条件加载 ❌ 不支持 ✅ 支持
返回值 直接绑定导出值 Promise

官方文档TC39: import()

静态结构

ESM 的 静态import/export 声明具有静态结构,在代码执行前就能确定模块依赖关系。这使得编译器和打包工具能在 Construction 阶段进行静态分析,带来诸多优化。

1. Tree-shaking(树摇优化)

打包工具能识别未使用的导出,移除死代码。

javascript 复制代码
// utils.js
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
} // 未被使用
export function multiply(a, b) {
  return a * b;
} // 未被使用

// main.js
import { add } from "./utils.js";
console.log(add(1, 2));

// 打包后:subtract 和 multiply 被移除

2. 循环依赖检测

构建工具能在编译时检测循环依赖。

javascript 复制代码
// a.js
import { b } from "./b.js";
export const a = 1;

// b.js
import { a } from "./a.js"; // 循环依赖
export const b = 2;

// 构建工具可在编译时发出警告

3. 导出验证

在 Instantiation 阶段验证所有导入是否有对应的导出。

javascript 复制代码
// math.js
export const add = (a, b) => a + b;

// main.js
import { add, subtract } from "./math.js";
// 错误:Instantiation 阶段报错
// SyntaxError: The requested module './math.js' does not provide an export named 'subtract'

值实时绑定,不能在导入后被修改

ESM 的导出和导入建立实时绑定(Live Bindings):import 和 export 指向内存中的同一位置,导出模块修改值时,导入模块能实时看到变化。

javascript 复制代码
// counter.js
export let count = 0;
export function increment() {
  count++;
}

// main.js
import { count, increment } from "./counter.js";

console.log(count); // 0
increment();
console.log(count); // 1 - 实时看到变化

// ❌ 导入的绑定是只读的
count = 10; // TypeError: Assignment to constant variable

原理 :在 Instantiation 阶段,count 在内存中只有一份,import 和 export 都指向这个位置。

graph LR A[counter.js: export count] -->|指向| C[内存地址 0x1234] B[main.js: import count] -->|指向| C D[counter.js: increment 修改 count] -->|修改| C

官方文档TC39: Module Environment Records | Node.js: Modules: Cycles

循环依赖处理

ESM 通过实时绑定三阶段加载优雅地处理循环依赖。

javascript 复制代码
// a.js
import { b } from "./b.js";
export let a = "a";
console.log("a.js:", b);

// b.js
import { a } from "./a.js";
export let b = "b";
console.log("b.js:", a); // ReferenceError: Cannot access 'a' before initialization

// main.js
import "./a.js";

执行流程

  1. Construction 阶段 :构建模块图,main.jsa.jsb.js
  2. Instantiation 阶段
    • a.jsa 在内存中分配空间(未赋值)
    • b.jsb 在内存中分配空间(未赋值)
    • 建立实时绑定:a.js 的 import 绑定到 b.js 的 export,反之亦然
  3. Evaluation 阶段 (深度优先后序遍历):
    • 先执行 b.js:此时 a 还未赋值,直接报错 Cannot access 'a' before initialization

关键点

  • Instantiation 阶段已建立所有绑定关系
  • Evaluation 阶段只是填充值
  • 实时绑定,并且被初始化后,才能访问到正确的值

支持 top-level await

ESM 支持在模块顶层直接使用 await,无需包裹在 async 函数中。CJS 模块被包装在普通函数中执行,不支持 top-level await。

javascript 复制代码
// data.js
const response = await fetch("https://api.example.com/data");
export const data = await response.json();

// main.js
import { data } from "./data.js";
console.log(data); // 等待 data.js 完成后执行

执行时机

  • top-level await 在 Evaluation 阶段执行
  • 当前模块会暂停,等待 Promise 完成
  • 依赖当前模块的父模块也会等待
graph TD A[main.js: import data.js] --> B[Construction: 构建模块图] B --> C[Instantiation: 建立绑定] C --> D[Evaluation: 执行 data.js] D --> E[data.js: await fetch...] E --> F[暂停, 等待 Promise 完成] F --> G[Promise 完成, 继续执行] G --> H[data.js 完成] H --> I[执行 main.js]

使用场景

javascript 复制代码
// 1. 动态加载配置
const config = await import(`./config.${process.env.NODE_ENV}.js`);
export default config;

// 2. 等待数据库连接
import mongoose from "mongoose";
await mongoose.connect(process.env.DB_URL);
export { mongoose };

// 3. 条件加载 polyfill
const locale = navigator.language;
if (!Intl.PluralRules) {
  await import(`https://cdn.example.com/polyfill/${locale}.js`);
}

注意事项

javascript 复制代码
// ⚠️ 阻塞效应:所有依赖此模块的模块都会等待
// slow-module.js
await new Promise((resolve) => setTimeout(resolve, 5000));
export const value = "done";

// main.js
import { value } from "./slow-module.js"; // 等待 5 秒
console.log(value);

// ⚠️ 错误处理:未捕获的 Promise rejection 会导致模块加载失败
await fetch("https://invalid-url.com/data"); // 整个模块加载失败

常见问题

ESM 相比 CJS 有什么好处?

1. 静态分析,支持 Tree-shaking

ESM 的 import/export 在编译时就能确定依赖关系,打包工具可以移除未使用的代码,显著减小打包体积。CJS 的 require 是运行时调用,无法静态分析。

2. 原生支持,无需打包

ESM 是 ECMAScript 标准,浏览器和 Node.js 原生支持。CJS 需要打包工具(Webpack、Browserify)转换才能在浏览器运行。

3. 异步加载,不阻塞渲染

ESM 在浏览器中异步加载,不会阻塞页面渲染(<script type="module"> 默认 defer)。CJS 同步加载不适合浏览器。

4. 实时绑定,动态反映变化

ESM 的导入导出是实时绑定,导出模块修改值时,导入模块能立即看到变化。CJS 是值拷贝,看不到后续变化。

5. 支持 Top-level await

ESM 可以在模块顶层直接使用 await,适合异步初始化场景(如数据库连接、配置加载)。CJS 不支持。

6. 更好的循环依赖处理

ESM 通过实时绑定处理循环依赖,虽然访问未初始化变量会报错(TDZ),但更容易发现问题。CJS 返回未完成的 exports,容易产生难以调试的 bug。

7. 统一的模块标准

ESM 是浏览器和 Node.js 通用的标准,同一套代码可以跨平台运行,无需维护两套模块系统。

在 Node.js 中 CJS 和 ESM 能否互相引用?

ESM 引用 CJS:✅ 支持

ESM 可以通过 import 引用 CJS 模块,Node.js 会将 module.exports 作为默认导出。

CJS 引用 ESM:❌ 不支持同步引用

CJS 的 require 是同步的,无法加载异步的 ESM 模块。必须使用动态 import() 函数(返回 Promise)。

官方文档Node.js: Interoperability with CJS

Node 中如何判断一个 .js 文件是 CJS 还是 ESM?

Node.js 通过以下规则判断:

  1. 文件扩展名.mjs 文件是 ESM,.cjs 文件是 CJS
  2. package.json 的 type 字段
    • "type": "module".js 文件视为 ESM
    • "type": "CJS" 或无 type 字段:.js 文件视为 CJS(默认)
  3. 查找最近的 package.json:从当前文件向上查找最近的 package.json

官方文档Node.js: Determining module system

ESM 和 CJS 的差异

维度 CJS ESM
定位 Node.js 运行时模块系统 ECMAScript 语言标准
加载时机 运行时(同步) 编译时静态分析
语法 require / module.exports import / export
值的导出 值拷贝(快照) 实时绑定(引用)
循环依赖 返回未完成的 exports 通过实时绑定处理,访问未初始化会报错
Top-level await ❌ 不支持 ✅ 支持
Tree-shaking ❌ 不支持 ✅ 支持
动态导入 ✅ 原生支持(require 本身) 需使用 import() 函数
相关推荐
幸运小圣3 小时前
深入理解ref、reactive【Vue3工程级指南】
前端·javascript·vue.js
用户47949283569153 小时前
面试官最爱挖的坑:用户 Token 到底该存哪?
前端·javascript·面试
Heo3 小时前
Vue3.4中diff算法核心梳理
前端·javascript·面试
阿蒙Amon3 小时前
JavaScript学习笔记:11.对象
javascript·笔记·学习
阿蒙Amon3 小时前
JavaScript学习笔记:9.数组
javascript·笔记·学习
幸运小圣3 小时前
【Vue3】 中 ref 与 reactive:状态与模型的深入理解
前端·javascript·vue.js
沐风。563 小时前
TypeScript
前端·javascript·typescript
NuLL4 小时前
全场景智能克隆工具:超越 JSON.parse(JSON.stringify())
javascript
编程小Y4 小时前
Vue 3 + Vite
前端·javascript·vue.js