0%

异步编程

目标:掌握 JavaScript 异步编程的核心概念,包括事件循环、Promise、async/await,以及各种异步模式和最佳实践。

目录

  1. 同步与异步、事件循环
  2. 异步方案逐项实战
  3. 高级异步模式
  4. 实战应用场景
  5. 代码片段速查
  6. 常见问题与解决方案
  7. 速记与总结

1. 同步与异步、事件循环

1.1 同步 vs 异步

理解同步和异步的区别是掌握 JavaScript 异步编程的基础。

1.1.1 同步执行(Synchronous)

同步代码按照书写顺序一行一行执行,前一行执行完成后才会执行下一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 同步代码示例
console.log('1. 开始');
console.log('2. 中间');
console.log('3. 结束');

// 输出顺序:
// 1. 开始
// 2. 中间
// 3. 结束

// 同步阻塞示例
console.log('开始计算');
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i; // 这个循环会阻塞后续代码执行
}
console.log('计算完成:', sum); // 要等循环结束才会执行
console.log('后续代码'); // 也要等上面执行完

同步的特点

  • ✅ 执行顺序可预测
  • ✅ 代码逻辑清晰
  • ❌ 阻塞后续代码执行
  • ❌ 长时间操作会导致页面卡顿

1.1.2 异步执行(Asynchronous)

异步代码不会阻塞后续代码的执行,任务会在后台执行,完成后通过回调函数通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 异步代码示例
console.log('1. 开始');

setTimeout(() => {
console.log('2. 异步任务完成');
}, 1000);

console.log('3. 结束');

// 输出顺序:
// 1. 开始
// 3. 结束
// 2. 异步任务完成(1秒后)

// 异步不阻塞示例
console.log('开始请求数据');
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log('数据获取完成:', data);
});
console.log('继续执行其他代码'); // 不会等待 fetch 完成

异步的特点

  • ✅ 不阻塞后续代码
  • ✅ 提高用户体验
  • ✅ 可以并发处理多个任务
  • ❌ 执行顺序不如同步代码直观
  • ❌ 需要回调或 Promise 处理结果

1.1.3 为什么需要异步?

场景1:网络请求

1
2
3
4
5
6
7
8
9
10
11
12
// 如果网络请求是同步的
const data = fetch('/api/data'); // 假设这是同步的
console.log(data); // 要等网络请求完成(可能几秒钟)
// 在这期间,页面完全卡住,用户无法操作

// 使用异步
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log(data); // 数据到达后处理
});
// 页面可以继续响应用户操作

场景2:定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 同步等待(不推荐)
function waitSync(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// 空循环,阻塞主线程
}
}
waitSync(1000); // 页面卡住1秒
console.log('1秒后');

// 异步等待(推荐)
setTimeout(() => {
console.log('1秒后');
}, 1000);
// 页面不卡顿,可以继续执行其他代码

场景3:文件操作(Node.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 同步读取文件(阻塞)
const fs = require('fs');
const data = fs.readFileSync('large-file.txt', 'utf8'); // 阻塞
console.log('文件读取完成');

// 异步读取文件(非阻塞)
fs.readFile('large-file.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件读取完成:', data);
});
console.log('继续执行其他代码'); // 不等待文件读取

1.2 事件循环(Event Loop)概览

事件循环是 JavaScript 实现异步的核心机制。理解事件循环有助于理解异步代码的执行顺序。

1.2.1 JavaScript 是单线程的

JavaScript 只有一个主线程(Main Thread),所有代码都在这个线程上执行。

1
2
3
4
5
// 单线程意味着同一时间只能执行一个任务
console.log('任务1');
console.log('任务2');
console.log('任务3');
// 必须按顺序执行,不能同时执行多个任务

为什么是单线程?

  • 简化编程模型(不需要处理线程同步、死锁等问题)
  • 适合处理 DOM 操作(多线程操作 DOM 会带来复杂的同步问题)

单线程的挑战

  • 如何在不阻塞的情况下处理耗时操作?
  • 答案:事件循环 + 异步 API

1.2.2 事件循环的组成部分

事件循环由以下几个部分组成:

1. 调用栈(Call Stack)

调用栈用于执行同步代码,遵循后进先出(LIFO)原则。

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
function a() {
console.log('a 开始');
b();
console.log('a 结束');
}

function b() {
console.log('b 开始');
c();
console.log('b 结束');
}

function c() {
console.log('c');
}

a();

// 调用栈变化:
// [a] -> a 开始
// [a, b] -> b 开始
// [a, b, c] -> c
// [a, b] -> b 结束
// [a] -> a 结束
// [] -> 栈空

2. 宏任务队列(Macro Task Queue)

宏任务包括:

  • setTimeout / setInterval
  • I/O 操作(文件读取、网络请求)
  • UI 渲染
  • script 标签执行
1
2
3
4
5
6
7
8
9
10
11
12
console.log('1. 同步代码');

setTimeout(() => {
console.log('2. setTimeout');
}, 0);

console.log('3. 同步代码');

// 输出:
// 1. 同步代码
// 3. 同步代码
// 2. setTimeout(即使延时是0,也要等同步代码执行完)

3. 微任务队列(Micro Task Queue)

微任务包括:

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver
1
2
3
4
5
6
7
8
9
10
11
12
console.log('1. 同步代码');

Promise.resolve().then(() => {
console.log('2. Promise.then');
});

console.log('3. 同步代码');

// 输出:
// 1. 同步代码
// 3. 同步代码
// 2. Promise.then(微任务优先于宏任务)

1.2.3 事件循环的执行顺序

事件循环的执行顺序可以用以下步骤概括:

1
2
3
4
5
1. 执行调用栈中的同步代码
2. 调用栈清空后,执行所有微任务
3. 执行一个宏任务
4. 再次执行所有微任务
5. 重复步骤 3-4

详细示例

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
console.log('1. 同步代码');

setTimeout(() => {
console.log('2. setTimeout(宏任务)');
}, 0);

Promise.resolve().then(() => {
console.log('3. Promise.then(微任务)');
});

queueMicrotask(() => {
console.log('4. queueMicrotask(微任务)');
});

console.log('5. 同步代码');

// 执行过程:
// 1. 执行同步代码:输出 1, 5
// 2. 调用栈清空,执行所有微任务:输出 3, 4
// 3. 执行一个宏任务:输出 2

// 最终输出:
// 1. 同步代码
// 5. 同步代码
// 3. Promise.then(微任务)
// 4. queueMicrotask(微任务)
// 2. setTimeout(宏任务)

复杂示例

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
console.log('1. 同步开始');

setTimeout(() => {
console.log('2. setTimeout 1');
Promise.resolve().then(() => {
console.log('3. Promise in setTimeout');
});
}, 0);

Promise.resolve().then(() => {
console.log('4. Promise 1');
setTimeout(() => {
console.log('5. setTimeout in Promise');
}, 0);
});

Promise.resolve().then(() => {
console.log('6. Promise 2');
});

console.log('7. 同步结束');

// 执行过程:
// 1. 执行同步代码:输出 1, 7
// 2. 执行微任务:输出 4, 6(Promise 1, Promise 2)
// - 在 Promise 1 中,setTimeout 被加入宏任务队列
// 3. 执行宏任务 setTimeout 1:输出 2
// 4. 执行微任务(Promise in setTimeout):输出 3
// 5. 执行宏任务(setTimeout in Promise):输出 5

// 最终输出:
// 1. 同步开始
// 7. 同步结束
// 4. Promise 1
// 6. Promise 2
// 2. setTimeout 1
// 3. Promise in setTimeout
// 5. setTimeout in Promise

1.2.4 微任务优先于宏任务

这是事件循环的重要规则:微任务会在宏任务之前执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('开始');

setTimeout(() => {
console.log('宏任务');
}, 0);

Promise.resolve().then(() => {
console.log('微任务');
});

console.log('结束');

// 输出:
// 开始
// 结束
// 微任务(先执行)
// 宏任务(后执行)

为什么微任务优先?

微任务通常用于:

  • Promise 回调
  • DOM 变更通知(MutationObserver)

这些操作需要在下一个宏任务之前完成,确保状态的一致性。

1.2.5 动画与渲染

浏览器会在宏任务之间执行渲染操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 渲染时机
console.log('1. 同步代码');

setTimeout(() => {
console.log('2. 宏任务1');
// 这里可能会执行渲染
}, 0);

setTimeout(() => {
console.log('3. 宏任务2');
// 这里可能会执行渲染
}, 0);

// 执行顺序:
// 1. 同步代码
// 2. 宏任务1 → 可能渲染
// 3. 宏任务2 → 可能渲染

requestAnimationFrame

requestAnimationFrame 会在下一次重绘之前执行,通常用于动画:

1
2
3
4
5
6
7
8
function animate() {
// 更新动画状态
element.style.left = position + 'px';

requestAnimationFrame(animate); // 在下一帧前执行
}

requestAnimationFrame(animate);

执行时机对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log('1. 同步代码');

setTimeout(() => {
console.log('2. setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('3. Promise');
});

requestAnimationFrame(() => {
console.log('4. requestAnimationFrame');
});

// 输出顺序:
// 1. 同步代码
// 3. Promise(微任务)
// 4. requestAnimationFrame(在渲染前)
// 2. setTimeout(宏任务)

1.3 回调函数与回调地狱

1.3.1 什么是回调函数?

回调函数是作为参数传递给另一个函数的函数,在特定时机被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 同步回调
function processArray(arr, callback) {
for (let item of arr) {
callback(item);
}
}

processArray([1, 2, 3], (item) => {
console.log(item);
});

// 异步回调
setTimeout(() => {
console.log('1秒后执行');
}, 1000);

// 事件回调
button.addEventListener('click', () => {
console.log('按钮被点击');
});

1.3.2 回调地狱(Callback Hell)

当多个异步操作需要串行执行时,会出现深层嵌套的回调,这就是”回调地狱”。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 回调地狱示例
getUser(userId, (user) => {
getProfile(user.id, (profile) => {
getPosts(profile.id, (posts) => {
getComments(posts[0].id, (comments) => {
renderComments(comments, () => {
console.log('完成');
// 已经嵌套了5层!
});
});
});
});
});

回调地狱的问题

  1. 代码可读性差:深层嵌套难以理解
  2. 错误处理困难:每个回调都要单独处理错误
  3. 控制流分散:逻辑分散在各个回调中
  4. 难以维护:修改一个环节需要修改多处代码

1.3.3 缓解回调地狱的方法

方法1:拆分成命名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 回调地狱
getUser(userId, (user) => {
getProfile(user.id, (profile) => {
getPosts(profile.id, (posts) => {
renderPosts(posts);
});
});
});

// ✅ 拆分成命名函数
function handleUser(user) {
getProfile(user.id, handleProfile);
}

function handleProfile(profile) {
getPosts(profile.id, handlePosts);
}

function handlePosts(posts) {
renderPosts(posts);
}

getUser(userId, handleUser);

方法2:使用 Promise

1
2
3
4
5
6
// ✅ 使用 Promise 链
getUser(userId)
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.id))
.then(posts => renderPosts(posts))
.catch(error => console.error(error));

方法3:使用 async/await

1
2
3
4
5
6
7
8
9
10
11
// ✅ 使用 async/await
async function loadData() {
try {
const user = await getUser(userId);
const profile = await getProfile(user.id);
const posts = await getPosts(profile.id);
renderPosts(posts);
} catch (error) {
console.error(error);
}
}

方法4:使用函数组合

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 使用函数组合(pipe)
const pipe = (...fns) => (value) =>
fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(value));

const loadData = pipe(
getUser,
user => getProfile(user.id),
profile => getPosts(profile.id),
renderPosts
);

loadData(userId).catch(console.error);

2. 异步方案逐项实战

2.1 定时器:setTimeout / setInterval

2.1.1 setTimeout 基础

setTimeout 用于在指定时间后执行一次函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基本语法
setTimeout(callback, delay, ...args);

// 示例
setTimeout(() => {
console.log('1秒后执行');
}, 1000);

// 传递参数
setTimeout((name, age) => {
console.log(`姓名: ${name}, 年龄: ${age}`);
}, 1000, '张三', 25);

// 保存定时器 ID,用于取消
const timerId = setTimeout(() => {
console.log('这行不会执行');
}, 1000);

clearTimeout(timerId); // 取消定时器

重要特性

  1. 最小延时:在浏览器中,setTimeout 的最小延时约为 4ms(即使设置为 0)
1
2
3
4
5
6
7
8
9
10
console.log('开始');
setTimeout(() => {
console.log('延时0ms');
}, 0);
console.log('结束');

// 输出:
// 开始
// 结束
// 延时0ms(不是立即执行)
  1. 不保证精确时间setTimeout 只保证至少在指定时间后执行,不保证精确
1
2
3
4
5
const start = Date.now();
setTimeout(() => {
const elapsed = Date.now() - start;
console.log(`实际延时: ${elapsed}ms`); // 可能大于 1000ms
}, 1000);
  1. 页面后台时会被节流:当标签页在后台时,浏览器会降低定时器的执行频率
1
2
3
4
// 在后台标签页中,这个定时器可能不会按预期执行
setInterval(() => {
console.log('每秒执行');
}, 1000);

2.1.2 setInterval 基础

setInterval 用于每隔指定时间重复执行函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 基本语法
setInterval(callback, delay, ...args);

// 示例
const intervalId = setInterval(() => {
console.log('每秒执行');
}, 1000);

// 5秒后停止
setTimeout(() => {
clearInterval(intervalId);
}, 5000);

setInterval 的问题

  1. 时间漂移:如果回调执行时间超过间隔时间,会导致时间漂移
1
2
3
4
5
// ❌ 问题:如果 process 耗时 600ms,间隔 500ms
setInterval(() => {
process(); // 假设耗时 600ms
}, 500);
// 实际间隔会变成 600ms+,而不是 500ms
  1. 重入问题:如果回调执行时间超过间隔,可能会同时执行多个回调
1
2
3
4
5
// ❌ 问题:如果 process 耗时超过间隔
setInterval(() => {
process(); // 如果耗时 600ms,间隔 500ms
}, 500);
// 可能导致多个 process 同时执行

解决方案:使用递归 setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 使用递归 setTimeout 替代 setInterval
function repeat(callback, delay) {
const timerId = setTimeout(() => {
callback();
repeat(callback, delay); // 递归调用
}, delay);
return timerId; // 返回 ID 用于取消
}

// 使用
let count = 0;
const timerId = repeat(() => {
console.log(count++);
if (count >= 5) {
clearTimeout(timerId);
}
}, 1000);

更完善的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function setIntervalFixed(callback, delay) {
let timerId;

function schedule() {
timerId = setTimeout(() => {
callback();
schedule(); // 在回调执行后再调度下一次
}, delay);
}

schedule();
return () => clearTimeout(timerId); // 返回取消函数
}

// 使用
const cancel = setIntervalFixed(() => {
console.log('执行');
}, 1000);

// 5秒后取消
setTimeout(cancel, 5000);

2.1.3 实际应用场景

场景1:防抖(Debounce)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function debounce(fn, delay) {
let timerId = null;
return function(...args) {
clearTimeout(timerId); // 清除之前的定时器
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

// 使用:搜索输入框
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
console.log('搜索:', query);
}, 300);

searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});

场景2:节流(Throttle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}

// 使用:滚动事件
const throttledScroll = throttle(() => {
console.log('滚动');
}, 100);

window.addEventListener('scroll', throttledScroll);

场景3:延迟执行

1
2
3
4
5
6
7
8
9
10
11
12
13
// 延迟显示提示
function showTooltip(element, message) {
const timerId = setTimeout(() => {
element.textContent = message;
element.style.display = 'block';
}, 500);

// 鼠标移开时取消
element.addEventListener('mouseleave', () => {
clearTimeout(timerId);
element.style.display = 'none';
});
}

2.2 Promise 基础与链式调用

2.2.1 Promise 的三种状态

Promise 有三种状态,状态转换是单向的:

1
pending(等待) → fulfilled(成功)或 rejected(失败)
1
2
3
4
5
6
7
8
9
10
11
12
// 创建 Promise
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
resolve('成功'); // 状态变为 fulfilled
// 或
// reject('失败'); // 状态变为 rejected
}, 1000);
});

// 状态检查
console.log(promise); // Promise { <pending> }

状态特点

  1. 状态不可逆:一旦状态改变,就不能再改变
1
2
3
4
5
6
7
8
const promise = new Promise((resolve, reject) => {
resolve('成功');
reject('失败'); // 无效,状态已经是 fulfilled
});

promise.then(value => {
console.log(value); // '成功'
});
  1. pending → fulfilled:调用 resolve(value)
1
2
3
4
5
6
7
const promise = new Promise((resolve) => {
resolve('成功值');
});

promise.then(value => {
console.log(value); // '成功值'
});
  1. pending → rejected:调用 reject(reason) 或抛出错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise1 = new Promise((resolve, reject) => {
reject('失败原因');
});

const promise2 = new Promise(() => {
throw new Error('错误');
});

promise1.catch(reason => {
console.log(reason); // '失败原因'
});

promise2.catch(error => {
console.log(error.message); // '错误'
});

2.2.2 Promise 的链式调用

thencatch 方法返回新的 Promise,可以链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 基本链式调用
Promise.resolve(1)
.then(value => {
console.log(value); // 1
return value * 2; // 返回新值
})
.then(value => {
console.log(value); // 2
return value * 3;
})
.then(value => {
console.log(value); // 6
});

返回值处理

  1. 返回普通值:会被包装成 fulfilled Promise
1
2
3
4
5
Promise.resolve(1)
.then(value => value * 2) // 返回 2
.then(value => {
console.log(value); // 2
});
  1. 返回 Promise:会等待这个 Promise 完成
1
2
3
4
5
6
7
8
9
Promise.resolve(1)
.then(value => {
return new Promise(resolve => {
setTimeout(() => resolve(value * 2), 1000);
});
})
.then(value => {
console.log(value); // 2(1秒后)
});
  1. 抛出错误:会被包装成 rejected Promise
1
2
3
4
5
6
7
Promise.resolve(1)
.then(value => {
throw new Error('错误');
})
.catch(error => {
console.log(error.message); // '错误'
});

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 串联异步操作
fetch('/api/user')
.then(response => response.json()) // 解析 JSON
.then(user => {
console.log('用户:', user);
return fetch(`/api/profile/${user.id}`); // 返回新的 Promise
})
.then(response => response.json())
.then(profile => {
console.log('资料:', profile);
})
.catch(error => {
console.error('错误:', error); // 捕获链中任何错误
});

2.2.3 错误冒泡

Promise 链中的错误会向下传播,直到被 catch 捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Promise.resolve(1)
.then(value => {
throw new Error('错误1');
})
.then(value => {
console.log('这行不会执行');
})
.catch(error => {
console.log('捕获错误:', error.message); // '错误1'
return '恢复值';
})
.then(value => {
console.log(value); // '恢复值'(catch 后继续执行)
});

错误处理策略

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
// 策略1:在链的末尾统一处理
fetch('/api/data')
.then(response => response.json())
.then(data => processData(data))
.catch(error => {
// 处理所有错误
console.error('操作失败:', error);
});

// 策略2:在中间处理特定错误
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('网络错误');
}
return response.json();
})
.catch(error => {
// 处理网络错误
if (error.message === '网络错误') {
return { data: [] }; // 返回默认值
}
throw error; // 重新抛出其他错误
})
.then(data => {
console.log(data);
})
.catch(error => {
// 处理其他错误
console.error('其他错误:', error);
});

2.2.4 finally 方法

finally 方法无论 Promise 是成功还是失败都会执行。

1
2
3
4
5
6
7
8
9
10
11
12
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log('成功:', data);
})
.catch(error => {
console.error('失败:', error);
})
.finally(() => {
console.log('无论成功失败都会执行');
// 通常用于清理工作
});

finally 的特点

  1. 返回值会被忽略(除非抛出错误)
1
2
3
4
5
6
7
8
Promise.resolve(1)
.then(value => value * 2)
.finally(() => {
return 999; // 返回值被忽略
})
.then(value => {
console.log(value); // 2(不是 999)
});
  1. 如果 finally 中抛出错误,会覆盖之前的返回值
1
2
3
4
5
6
7
8
9
10
11
Promise.resolve(1)
.then(value => value * 2)
.finally(() => {
throw new Error('finally 错误');
})
.then(value => {
console.log('不会执行');
})
.catch(error => {
console.log(error.message); // 'finally 错误'
});

2.3 async/await

async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。

2.3.1 async 函数

async 函数总是返回 Promise。

1
2
3
4
5
6
7
8
9
10
11
12
// async 函数返回 Promise
async function getData() {
return '数据';
}

const promise = getData();
console.log(promise); // Promise { <fulfilled>: '数据' }

// 等价于
function getData() {
return Promise.resolve('数据');
}

async 函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 返回普通值 → 包装成 fulfilled Promise
async function getValue() {
return 42;
}
getValue().then(value => console.log(value)); // 42

// 返回 Promise → 直接返回
async function getPromise() {
return Promise.resolve('值');
}
getPromise().then(value => console.log(value)); // '值'

// 抛出错误 → 包装成 rejected Promise
async function throwError() {
throw new Error('错误');
}
throwError().catch(error => console.log(error.message)); // '错误'

2.3.2 await 关键字

await 等待 Promise 完成,只能在 async 函数中使用(或顶层 ES 模块)。

1
2
3
4
5
6
7
8
9
10
11
12
// await 等待 Promise 完成
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}

// 等价于
function fetchData() {
return fetch('/api/data')
.then(response => response.json());
}

await 的行为

  1. 等待 Promise 完成:如果 Promise 是 pending,会暂停函数执行
1
2
3
4
5
6
7
8
9
10
11
async function test() {
console.log('开始');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('1秒后');
}

test();
// 输出:
// 开始
// (等待1秒)
// 1秒后
  1. 返回 resolved 值:如果 Promise fulfilled,返回其值
1
2
3
4
async function test() {
const value = await Promise.resolve(42);
console.log(value); // 42
}
  1. 抛出 rejected 值:如果 Promise rejected,抛出错误
1
2
3
4
5
6
7
async function test() {
try {
await Promise.reject('错误');
} catch (error) {
console.log(error); // '错误'
}
}

2.3.3 串行 vs 并行

串行执行(一个接一个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 串行(慢)
async function serial() {
const a = await taskA(); // 等待 taskA 完成
const b = await taskB(); // 等待 taskB 完成
return [a, b];
}
// 总耗时 = taskA 耗时 + taskB 耗时

// ✅ 并行(快)
async function parallel() {
const [a, b] = await Promise.all([
taskA(), // 同时开始
taskB() // 同时开始
]);
return [a, b];
}
// 总耗时 = max(taskA 耗时, taskB 耗时)

实际示例

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
// 串行:依次获取用户和帖子
async function loadUserAndPosts(userId) {
const user = await fetch(`/api/users/${userId}`);
const userData = await user.json();

const posts = await fetch(`/api/users/${userId}/posts`);
const postsData = await posts.json();

return { user: userData, posts: postsData };
}

// 并行:同时获取用户和帖子
async function loadUserAndPostsParallel(userId) {
const [userRes, postsRes] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/posts`)
]);

const [userData, postsData] = await Promise.all([
userRes.json(),
postsRes.json()
]);

return { user: userData, posts: postsData };
}

有依赖关系的串行

1
2
3
4
5
6
// 有依赖关系:必须先获取用户,再获取帖子
async function loadData(userId) {
const user = await fetchUser(userId); // 必须先获取用户
const posts = await fetchPosts(user.id); // 依赖 user.id
return { user, posts };
}

2.3.4 错误处理

方法1:try-catch

1
2
3
4
5
6
7
8
9
10
11
12
13
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('请求失败');
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error; // 重新抛出
}
}

方法2:catch 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
async function fetchData() {
const response = await fetch('/api/data')
.catch(error => {
console.error('网络错误:', error);
return null;
});

if (!response) {
return null;
}

return response.json();
}

方法3:封装安全函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 封装安全函数,返回 [error, data]
async function safe(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}

// 使用
const [error, data] = await safe(fetch('/api/data'));
if (error) {
console.error('错误:', error);
} else {
console.log('数据:', data);
}

2.4 并发工具:Promise.all / race / allSettled / any

2.4.1 Promise.all

Promise.all 等待所有 Promise 完成,如果有一个失败,整体失败。

1
2
3
4
5
6
7
8
// 语法
Promise.all([promise1, promise2, ...])
.then([value1, value2, ...] => {
// 所有 Promise 都成功
})
.catch(error => {
// 任何一个 Promise 失败
});

特点

  1. 全部成功才成功:所有 Promise 都 fulfilled 时,返回值的数组
1
2
3
4
5
6
7
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then(values => {
console.log(values); // [1, 2, 3]
});
  1. 一个失败就失败:任何一个 Promise rejected,整体 rejected
1
2
3
4
5
6
7
Promise.all([
Promise.resolve(1),
Promise.reject('错误'),
Promise.resolve(3)
]).catch(error => {
console.log(error); // '错误'
});
  1. 返回值顺序:返回值的顺序与输入顺序一致
1
2
3
4
5
6
7
Promise.all([
delay(100, 'a'), // 100ms 后返回 'a'
delay(50, 'b'), // 50ms 后返回 'b'
delay(200, 'c') // 200ms 后返回 'c'
]).then(values => {
console.log(values); // ['a', 'b', 'c'](顺序不变)
});

实际应用

1
2
3
4
5
6
7
8
9
10
// 并行请求多个 API
async function loadDashboard() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);

return { users, posts, comments };
}

2.4.2 Promise.race

Promise.race 返回第一个完成的 Promise(无论成功还是失败)。

1
2
3
4
5
6
7
8
// 语法
Promise.race([promise1, promise2, ...])
.then(value => {
// 第一个完成的 Promise 的值
})
.catch(error => {
// 如果第一个完成的是 rejected
});

特点

  1. 第一个完成就返回:无论是 fulfilled 还是 rejected
1
2
3
4
5
6
7
Promise.race([
delay(100, 'a'),
delay(50, 'b'),
delay(200, 'c')
]).then(value => {
console.log(value); // 'b'(最快完成)
});
  1. 如果第一个是 rejected,整体 rejected
1
2
3
4
5
6
Promise.race([
Promise.reject('错误'),
delay(100, '成功')
]).catch(error => {
console.log(error); // '错误'
});

实际应用:超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function withTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), timeoutMs)
);

return Promise.race([promise, timeout]);
}

// 使用
withTimeout(fetch('/api/data'), 5000)
.then(response => response.json())
.catch(error => {
if (error.message === '超时') {
console.error('请求超时');
} else {
console.error('其他错误:', error);
}
});

2.4.3 Promise.allSettled

Promise.allSettled 等待所有 Promise 完成(无论成功还是失败),返回每个 Promise 的结果。

1
2
3
4
5
// 语法
Promise.allSettled([promise1, promise2, ...])
.then(results => {
// results 是数组,每个元素是 { status, value/reason }
});

返回值格式

1
2
3
4
5
6
7
8
9
10
11
12
Promise.allSettled([
Promise.resolve(1),
Promise.reject('错误'),
Promise.resolve(3)
]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: '错误' },
// { status: 'fulfilled', value: 3 }
// ]
});

实际应用:部分成功处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function loadMultipleUsers(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`)
.then(r => r.json())
.catch(error => ({ error, id }))
);

const results = await Promise.allSettled(promises);

const users = [];
const errors = [];

results.forEach((result, index) => {
if (result.status === 'fulfilled') {
users.push(result.value);
} else {
errors.push({ userId: userIds[index], error: result.reason });
}
});

return { users, errors };
}

2.4.4 Promise.any

Promise.any 返回第一个成功的 Promise,如果全部失败,抛出 AggregateError

1
2
3
4
5
6
7
8
// 语法
Promise.any([promise1, promise2, ...])
.then(value => {
// 第一个成功的值
})
.catch(error => {
// 如果全部失败,error 是 AggregateError
});

特点

  1. 第一个成功就返回
1
2
3
4
5
6
7
Promise.any([
Promise.reject('错误1'),
delay(100, '成功'),
Promise.reject('错误2')
]).then(value => {
console.log(value); // '成功'
});
  1. 全部失败才失败
1
2
3
4
5
6
7
8
Promise.any([
Promise.reject('错误1'),
Promise.reject('错误2'),
Promise.reject('错误3')
]).catch(error => {
console.log(error instanceof AggregateError); // true
console.log(error.errors); // ['错误1', '错误2', '错误3']
});

实际应用:多服务器容错

1
2
3
4
5
6
7
8
9
10
11
async function fetchFromMultipleServers(urls) {
const promises = urls.map(url => fetch(url));

try {
const response = await Promise.any(promises);
return await response.json();
} catch (error) {
console.error('所有服务器都失败:', error.errors);
throw new Error('无法连接到任何服务器');
}
}

四种方法的对比

方法 成功条件 失败条件 返回值
Promise.all 全部成功 一个失败 值数组
Promise.race 第一个完成 第一个失败 第一个值
Promise.allSettled 全部完成 不会失败 结果数组
Promise.any 一个成功 全部失败 第一个成功值

2.5 异步错误处理

2.5.1 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
// 方式1:在链的末尾统一处理
fetch('/api/data')
.then(response => response.json())
.then(data => processData(data))
.catch(error => {
// 处理所有错误
console.error('操作失败:', error);
});

// 方式2:在中间处理特定错误
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('网络错误');
}
return response.json();
})
.catch(error => {
if (error.message === '网络错误') {
return { data: [] }; // 返回默认值
}
throw error; // 重新抛出其他错误
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('其他错误:', error);
});

2.5.2 async/await 的错误处理

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
// 方式1:try-catch
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}

// 方式2:封装安全函数
async function safe(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}

// 使用
const [error, data] = await safe(fetch('/api/data'));
if (error) {
console.error('错误:', error);
} else {
console.log('数据:', data);
}

2.5.3 全局错误处理

浏览器环境

1
2
3
4
5
6
7
8
9
10
// 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise rejection:', event.reason);
event.preventDefault(); // 阻止默认行为(在控制台显示错误)
});

// 捕获同步错误
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
});

Node.js 环境

1
2
3
4
5
6
7
8
9
10
// 捕获未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
});

// 捕获未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
process.exit(1); // 通常应该退出进程
});

2.6 柯里化与函数组合在异步中的应用

2.6.1 柯里化(Currying)

柯里化是将多参数函数转换为一系列单参数函数的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 普通函数
function add(a, b) {
return a + b;
}

// 柯里化版本
function addCurried(a) {
return function(b) {
return a + b;
};
}

// 使用
const add5 = addCurried(5);
console.log(add5(3)); // 8

// 箭头函数版本
const addCurriedArrow = a => b => a + b;
const add5Arrow = addCurriedArrow(5);
console.log(add5Arrow(3)); // 8

异步中的柯里化

1
2
3
4
5
6
7
8
9
10
11
// 延迟函数
const delay = ms => value =>
new Promise(resolve => setTimeout(() => resolve(value), ms));

// 创建特定延时的函数
const after1s = delay(1000);
const after2s = delay(2000);

// 使用
after1s('1秒后').then(console.log);
after2s('2秒后').then(console.log);

2.6.2 函数组合(Compose/Pipe)

函数组合是将多个函数组合成一个函数的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Pipe(从左到右)
const pipe = (...fns) => (value) =>
fns.reduce((acc, fn) => fn(acc), value);

// Compose(从右到左)
const compose = (...fns) => (value) =>
fns.reduceRight((acc, fn) => fn(acc), value);

// 使用
const add1 = x => x + 1;
const multiply2 = x => x * 2;
const square = x => x * x;

const pipeline = pipe(add1, multiply2, square);
console.log(pipeline(3)); // ((3+1)*2)^2 = 64

const composed = compose(square, multiply2, add1);
console.log(composed(3)); // ((3+1)*2)^2 = 64(结果相同)

异步函数组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 异步 Pipe
const pipeAsync = (...fns) => (value) =>
fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(value));

// 使用
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};

const fetchProfile = async (user) => {
const res = await fetch(`/api/profiles/${user.id}`);
return res.json();
};

const render = (profile) => {
console.log('渲染:', profile);
};

const flow = pipeAsync(fetchUser, fetchProfile, render);

flow(1).catch(console.error);

3. 高级异步模式

3.1 异步迭代器(Async Iterators)

ES2018 引入了异步迭代器,用于处理异步数据流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 异步生成器
async function* asyncGenerator() {
for (let i = 0; i < 3; i++) {
await delay(100);
yield i;
}
}

// 使用 for await...of
(async () => {
for await (const value of asyncGenerator()) {
console.log(value);
}
})();

3.2 异步队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AsyncQueue {
constructor() {
this.queue = [];
this.resolvers = [];
}

async enqueue(item) {
if (this.resolvers.length > 0) {
const resolve = this.resolvers.shift();
resolve(item);
} else {
this.queue.push(item);
}
}

async dequeue() {
if (this.queue.length > 0) {
return this.queue.shift();
}
return new Promise(resolve => {
this.resolvers.push(resolve);
});
}
}

4. 实战应用场景

4.1 数据加载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function loadUserDashboard(userId) {
try {
// 并行加载独立数据
const [user, settings] = await Promise.all([
fetchUser(userId),
fetchSettings(userId)
]);

// 串行加载依赖数据
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);

return { user, settings, posts, comments };
} catch (error) {
console.error('加载失败:', error);
throw error;
}
}

4.2 重试机制

1
2
3
4
5
6
7
8
9
10
11
12
13
async function retry(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}

// 使用
const data = await retry(() => fetch('/api/data').then(r => r.json()));

4.3 并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function concurrentLimit(tasks, limit) {
const results = [];
const executing = [];

for (const task of tasks) {
const promise = task().then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});

results.push(promise);
executing.push(promise);

if (executing.length >= limit) {
await Promise.race(executing);
}
}

return Promise.all(results);
}

5. 代码片段速查

5.1 手写一个最小 Promise 封装

1
2
3
4
5
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}

delay(200, 'ok').then(console.log);

5.2 用 race 做超时保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms)
);
return Promise.race([promise, timeout]);
}

// 使用
withTimeout(fetch('/api/data'), 5000)
.then(response => response.json())
.catch(error => {
if (error.message === 'timeout') {
console.error('请求超时');
}
});

5.3 串行与并行对比

1
2
3
4
5
6
7
8
9
10
11
12
// 串行
async function serial() {
const a = await taskA();
const b = await taskB(a);
return b;
}

// 并行
async function parallel() {
const [a, b] = await Promise.all([taskA(), taskB()]);
return [a, b];
}

5.4 安全执行器(避免到处 try-catch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const safe = fn => async (...args) => {
try {
return [null, await fn(...args)];
} catch (err) {
return [err, null];
}
};

const getUserSafe = safe(getUser);
const [err, user] = await getUserSafe(1);
if (err) {
console.error('错误:', err);
} else {
console.log('用户:', user);
}

5.5 防抖/节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 防抖:延迟执行,频繁触发时重置计时器
function debounce(fn, wait = 200) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}

// 节流:限制执行频率
function throttle(fn, wait = 100) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}

6. 常见问题与解决方案

6.1 await 在循环中

1
2
3
4
5
6
7
8
9
// ❌ 串行执行(慢)
for (const id of userIds) {
const user = await fetchUser(id); // 一个接一个
}

// ✅ 并行执行(快)
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);

6.2 Promise 链中的错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 错误被吞掉
Promise.resolve()
.then(() => {
throw new Error('错误');
})
.then(() => {
console.log('这行不会执行');
}); // 错误没有被处理

// ✅ 正确处理错误
Promise.resolve()
.then(() => {
throw new Error('错误');
})
.catch(error => {
console.error('捕获错误:', error);
});

6.3 async 函数中的 return

1
2
3
4
5
6
7
8
9
10
// ❌ 忘记 await
async function getData() {
return fetch('/api/data'); // 返回 Promise,不是数据
}

// ✅ 正确使用 await
async function getData() {
const response = await fetch('/api/data');
return response.json();
}

7. 速记与总结

7.1 核心概念速记

  • “同步阻塞栈,异步排队等栈空;微任务优先于宏任务。”

    • 同步代码在调用栈中执行,异步代码排队等待;微任务(Promise)优先于宏任务(setTimeout)。
  • “Promise 链返回值自动包;抛错/拒绝走最近 catch。”

    • Promise 链中返回普通值会自动包装成 Promise;错误会向下传播到最近的 catch。
  • “async 返回 Promise,await 只在 async 里用;并行用 all,超时用 race。”

    • async 函数返回 Promise;await 只能在 async 函数中使用;并行用 Promise.all,超时用 Promise.race。
  • “回调地狱不怕:拆函数、Promise 化、async 化、再加组合/管道。”

    • 解决回调地狱的方法:拆分函数、使用 Promise、使用 async/await、使用函数组合。
  • “定时器不精确,递归 setTimeout 可控;后台标签页会被节流。”

    • setTimeout 不保证精确时间;使用递归 setTimeout 可以避免 setInterval 的问题;后台标签页会被浏览器节流。

7.2 选择指南

场景 推荐方案 原因
简单异步操作 Promise 简单直接
复杂异步流程 async/await 代码清晰
并行请求 Promise.all 性能好
超时控制 Promise.race 简单有效
部分成功处理 Promise.allSettled 不会因一个失败而整体失败
多服务器容错 Promise.any 一个成功即可

7.3 最佳实践

  1. 优先使用 async/await:代码更清晰易读
  2. 并行处理独立操作:使用 Promise.all 提高性能
  3. 正确处理错误:使用 try-catch 或 catch 方法
  4. 避免在循环中使用 await:使用 Promise.all 并行处理
  5. 使用防抖/节流:优化频繁触发的事件
  6. 设置超时:避免长时间等待

结语:异步编程是 JavaScript 的核心特性之一。掌握 Promise、async/await 和各种异步模式,能够帮助你编写更高效、更易维护的代码。记住,理解事件循环是掌握异步编程的关键。