012-ESModule与CommonJS模块比较

ES Module 与 CommonJS 的核心差异

1. 语法与设计理念

  • CommonJS (CJS)

    • 语法:require() 导入,module.exportsexports 导出。
    • 设计目标:服务端模块化(如 Node.js),同步加载,运行时解析依赖。
      1
      2
      3
      4
      // 导出
      module.exports = { key: 'value' };
      // 导入
      const obj = require('./module');
  • ES Module (ESM)

    • 语法:import 导入,export 导出。
    • 设计目标:浏览器与跨平台,静态编译(编译时分析依赖),支持异步加载(如 import())。
      1
      2
      3
      4
      // 导出
      export const key = 'value';
      // 导入
      import { key } from './module.mjs';

2. 加载机制与值传递

特性 CommonJS ES Module
加载时机 运行时同步加载 编译时解析,运行时执行
值传递 导出值的拷贝 导出值的引用(动态绑定)
循环依赖处理 通过缓存避免死循环,可能输出未完成状态的值 引用未初始化变量可能为 undefined(静态分析报错优先)
作用域 模块级作用域(通过函数包裹) 原生词法作用域

3. 异步支持与适用场景

  • CJS:仅同步加载,适用于服务端(文件读取快),浏览器端易阻塞渲染。
  • ESM:支持异步加载(import()),适用于浏览器与现代 Node.js,支持按需加载和 Tree Shaking。

4. 其他关键差异

  • 顶层 this 指向:CJS 指向当前模块,ESM 指向 undefined
  • 动态表达式:CJS 的 require() 可动态调用(如 require(path)),ESM 的 import 路径需静态字符串(动态路径需 import())。

互相加载的方式

1. ESM 加载 CommonJS

  • 默认行为:ESM 的 import 会将 CJS 的 module.exports 视为 default 导出。
    1
    2
    3
    4
    5
    // CJS 模块 (math.cjs)
    module.exports = { add: (a, b) => a + b };

    // ESM 导入
    import math from './math.cjs'; // math = { add: [Function] }
  • 命名导入的限制
    需通过解构从 default 中提取,直接命名导入会报错:
    1
    2
    3
    4
    5
    6
    // 错误!
    import { add } from './math.cjs';

    // 正确:解构 default
    import cjsModule from './math.cjs';
    const { add } = cjsModule;

2. CommonJS 加载 ESM

  • 问题:CJS 的 require() 是同步的,而 ESM 需异步加载。
  • 解决方案:使用动态 import()(返回 Promise):
    1
    2
    3
    4
    5
    // CJS 中加载 ESM
    (async () => {
    const esmModule = await import('./module.mjs');
    console.log(esmModule.key);
    })();

3. 兼容方案

  • 统一封装:若需同时支持两种规范,可创建适配层:
    1
    2
    3
    // 适配 CJS 模块为 ESM (wrapper.mjs)
    import cjsModule from './legacy.cjs';
    export const { propA, propB } = cjsModule;

与 AMD 的关系及差异

AMD 的角色

  • 定位:浏览器端异步加载的早期方案(如 RequireJS),解决 CJS 同步加载阻塞问题。
  • 语法define 定义模块,require 异步加载:
    1
    2
    3
    define(['dep1', 'dep2'], (dep1, dep2) => {
    return { key: dep1.value + dep2.value };
    });
  • 对比 ESM/CJS
    特性 AMD ESM CommonJS
    加载方式 异步 静态/支持异步 同步
    适用环境 浏览器 浏览器/Node.js Node.js
    依赖处理 依赖前置 静态分析 运行时解析

演进关系

AMD 是 ESM 成熟前浏览器的过渡方案,ESM 成为标准后逐渐被取代,但动态加载思想(如 import())吸收了其优点。


总结:如何选择?

  • Node.js 环境:传统库用 CommonJS,新项目首选 ESM(设置 "type": "module")。
  • 浏览器环境ES Module(原生支持 + 异步能力)。
  • 历史项目:AMD 仅需在维护旧浏览器代码时考虑。

通过 import/require 混用和动态加载,可实现渐进迁移,但需注意值传递和加载时序的差异。

013-Promise.all-race-allSettled概念+手撕

Promise 并发控制方法详解

一、核心静态方法概念

方法 描述 特性 结果
Promise.all(iterable) 等待所有 Promise 成功 短路特性(一个失败立即拒绝) 成功:结果数组
失败:第一个错误
Promise.race(iterable) 采用第一个完成的 Promise 竞速机制 第一个落定的结果(无论成功/失败)
Promise.allSettled(iterable) 等待所有 Promise 完成 不短路,收集所有结果 状态描述对象数组

二、手写实现

1. 手写 Promise.all
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;

promises.forEach((promise, i) => {
Promise.resolve(promise)
.then(value => {
results[i] = value;
if (++completed === promises.length) resolve(results);
})
.catch(reject); // 任一失败立即拒绝
});
});
};
2. 手写 Promise.race
1
2
3
4
5
6
7
8
9
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve)
.catch(reject);
});
});
};
3. 手写 Promise.allSettled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Promise.myAllSettled = function(promises) {
return new Promise(resolve => {
const results = [];
let completed = 0;

const checkCompletion = () => {
if (++completed === promises.length) resolve(results);
};

promises.forEach((promise, i) => {
Promise.resolve(promise)
.then(value => {
results[i] = { status: 'fulfilled', value };
})
.catch(reason => {
results[i] = { status: 'rejected', reason };
})
.finally(checkCompletion);
});
});
};

三、使用 Promise 实现请求并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class RequestPool {
constructor(maxConcurrent) {
this.max = maxConcurrent; // 最大并发数
this.queue = []; // 等待队列
this.running = 0; // 当前运行数
}

// 添加请求到池中
add(requestFn) {
return new Promise((resolve, reject) => {
const task = () => {
this.running++;
requestFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
};

// 立即执行或加入队列
if (this.running < this.max) {
task();
} else {
this.queue.push(task);
}
});
}

// 执行下一个任务
next() {
if (this.queue.length > 0 && this.running < this.max) {
const task = this.queue.shift();
task();
}
}
}
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 创建并发池(最大3个)
const pool = new RequestPool(3);

// 2. 模拟异步请求函数
const createRequest = (id, delay) => () =>
new Promise(resolve =>
setTimeout(() => resolve(`请求${id}完成`), delay)
);

// 3. 添加10个请求
for (let i = 1; i <= 10; i++) {
pool.add(createRequest(i, Math.random() * 2000))
.then(console.log); // 按完成顺序输出
}

/* 输出特点:
1. 同时最多3个请求并行
2. 完成一个立即补位下一个
3. 最终按实际完成顺序输出结果 */

四、关键实现原理

  1. 并发控制核心逻辑

    • 任务队列:存储等待执行的任务
    • 运行计数器:跟踪当前执行中的任务数
    • 自动补位机制:任务完成时自动触发下一个任务
  2. Promise 方法实现要点

    • 正确处理非 Promise 值(Promise.resolve()包装)
    • 保持结果顺序(数组索引定位)
    • 短路处理(all的立即拒绝)

五、实际应用场景

场景 推荐方法 说明
批量表单提交 Promise.all 需要全部成功,任一失败终止
竞速请求 Promise.race 获取最快响应(如CDN检测)
批量数据采集 Promise.allSettled 需要完整结果(无论成败)
图片懒加载 并发控制 避免同时加载过多图片
API 分页请求 并发控制 控制分页请求并发数量

六、特殊处理技巧

  1. 错误重试机制
1
pool.add(() => fetch(url).catch(() => fetch(url))) // 失败自动重试
  1. 优先级队列
1
2
3
4
5
// 高优先级任务
pool.queue.unshift(highPriorityTask);

// 低优先级任务
pool.queue.push(lowPriorityTask);
  1. 超时控制
1
2
3
4
5
6
pool.add(() => 
Promise.race([
fetch(url),
new Promise((_, r) => setTimeout(() => r('超时'), 5000))
])
);

性能提示:浏览器通常有 6-8 个 TCP 连接限制,合理设置并发数可优化性能