JavaScript 异步编程:Promise 与 async/await
JavaScript 异步编程:Promise 与 async/await
一、Introduction(简介)
JavaScript 是单线程语言,同一时刻只能执行一个任务。异步编程是 JavaScript 处理耗时操作(网络请求、文件读写、定时器)的核心机制,它避免了阻塞主线程,让程序在等待 I/O 完成期间继续执行其他任务。
在 ES6 之前,异步编程主要依赖回调函数(Callback),但回调地狱(Callback Hell)使代码难以阅读和维护。ES6 引入的 Promise 提供了更清晰的异步流程控制,而 ES2017 的 async/await 则进一步将异步代码写成同步风格,大幅提升了代码可读性。
本篇文章系统讲解 JavaScript 异步编程的演进历程、Promise 的原理与用法、async/await 语法、以及常见的异步流程控制模式(串行、并行、竞速等),帮助读者彻底掌握现代 JavaScript 异步编程。
二、基础语法
2.1 回调函数(Callback)— 异步的起点
回调是最原始的异步处理方式,将函数作为参数传入,在异步操作完成后调用。
// 传统的回调函数示例:读取文件(Node.js 风格)
const fs = require('fs');
fs.readFile('./data.txt', 'utf8', function(err, data) {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件内容:', data);
});
// 回调地狱(嵌套过深,难以维护)
fs.readFile('./a.txt', 'utf8', function(err, dataA) {
if (err) return console.error(err);
fs.readFile('./b.txt', 'utf8', function(err, dataB) {
if (err) return console.error(err);
fs.readFile('./c.txt', 'utf8', function(err, dataC) {
if (err) return console.error(err);
console.log(dataA, dataB, dataC);
});
});
});
2.2 Promise 的基本概念
Promise 是一个对象,代表一个异步操作的最终结果(成功值或失败原因)。Promise 有三种状态:
- Pending(待定):初始状态,操作进行中。
- Fulfilled(已兑现):操作成功,Promise 有了最终结果值。
- Rejected(已拒绝):操作失败,Promise 有了失败原因。
Promise 状态一旦改变(从 Pending 变为 Fulfilled 或 Rejected),就不可再变。
// 创建 Promise
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功,结果是 42');
} else {
reject(new Error('操作失败'));
}
}, 1000);
});
// 消费 Promise
promise
.then(result => {
console.log('Fulfilled:', result);
})
.catch(error => {
console.error('Rejected:', error.message);
})
.finally(() => {
console.log('无论成功或失败,都会执行');
});
2.3 Promise 链式调用
Promise 的 .then() 方法返回一个新的 Promise,支持链式调用,是解决回调地狱的关键。
// 链式调用:每个 .then() 返回的值会传递给下一个 .then()
fetchUserData()
.then(user => fetchUserPosts(user.id)) // 接收上一步的结果
.then(posts => filterPublishedPosts(posts))
.then(published => sortByDate(published))
.then(sorted => renderPosts(sorted))
.catch(err => console.error('出错了:', err));
// 模拟上述函数
function fetchUserData() {
return Promise.resolve({ id: 1, name: 'Alice' });
}
function fetchUserPosts(userId) {
return Promise.resolve([
{ id: 101, title: 'Post A', published: true },
{ id: 102, title: 'Post B', published: false },
]);
}
function filterPublishedPosts(posts) {
return Promise.resolve(posts.filter(p => p.published));
}
function sortByDate(posts) {
return Promise.resolve(posts.reverse());
}
function renderPosts(posts) {
console.log('渲染文章:', posts);
return posts;
}
2.4 Promise.all — 并行执行多个异步任务
当多个异步任务相互独立,可以并行执行时,使用 Promise.all 等待所有任务完成。
// 三个独立的网络请求,并行执行
const fetchUser = () => fetch('/api/user/1').then(r => r.json());
const fetchPosts = () => fetch('/api/posts').then(r => r.json());
const fetchComments = () => fetch('/api/comments').then(r => r.json());
// Promise.all:全部成功才进入 then,有一个失败就进入 catch
Promise.all([fetchUser(), fetchPosts(), fetchComments()])
.then(([user, posts, comments]) => {
console.log('用户:', user);
console.log('文章:', posts);
console.log('评论:', comments);
return { user, posts, comments };
})
.catch(err => {
console.error('某个请求失败了:', err);
});
// Promise.allSettled:无论成功失败都返回结果(ES2020)
Promise.allSettled([fetchUser(), fetchPosts(), fetchComments()])
.then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(${i} 成功:, result.value);
} else {
console.log(${i} 失败:, result.reason);
}
});
});
2.5 Promise.race 与 Promise.any
// Promise.race:返回最先完成(成功或失败)的 Promise 的结果
Promise.race([
fetch('/api/fast'),
new Promise((_, reject) => setTimeout(() => reject(new Error('超时')), 5000))
]).then(result => console.log(result))
.catch(err => console.error('超时或出错:', err.message));
// Promise.any:返回最先成功完成的 Promise 的结果,忽略失败(ES2021)
// 适用于:多个 CDN 同时请求,用最快响应的那个
Promise.any([
fetch('https://cdn1.example.com/resource'),
fetch('https://cdn2.example.com/resource'),
fetch('https://cdn3.example.com/resource'),
]).then(result => console.log('最快响应:', result))
.catch(err => console.error('所有 CDN 都失败了:', err.errors));
2.6 async/await 语法
async/await 是 ES2017 引入的语法糖,让异步代码读起来像同步代码。async 函数自动返回 Promise;await 等待 Promise resolve,并获取其结果值。
// async 函数声明
async function fetchUserInfo(userId) {
// await 暂停函数执行,等待 Promise 完成
const user = await fetch(/api/user/${userId}).then(r => r.json());
const posts = await fetch(/api/posts?userId=${userId}).then(r => r.json());
return { user, posts };
}
// async 函数也可以用箭头函数
const getData = async (id) => {
const data = await fetchData(id);
return data;
};
// 调用 async 函数(返回 Promise)
fetchUserInfo(1).then(({ user, posts }) => {
console.log(user, posts);
});
// 配合 try/catch 处理错误
async function loadContent() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (err) {
console.error('加载失败:', err);
}
}
2.7 并行优化:同时 await 多个 Promise
// 串行:逐个等待(总耗时 = a + b + c)
async function serial() {
const a = await taskA(); // 1秒
const b = await taskB(); // 1秒
const c = await taskC(); // 1秒
return [a, b, c];
}
// 并行:同时启动(总耗时 ≈ max(a, b, c))
async function parallel() {
// 同时发起三个任务,不等待
const [a, b, c] = await Promise.all([taskA(), taskB(), taskTaskC()]);
return [a, b, c];
}
// 示例:三个耗时1秒的任务
const taskA = () => new Promise(r => setTimeout(() => { console.log('A完成'); r('A'); }, 1000));
const taskB = () => new Promise(r => setTimeout(() => { console.log('B完成'); r('B'); }, 1000));
const taskC = () => new Promise(r => setTimeout(() => { console.log('C完成'); r('C'); }, 1000));
// 测试
console.time('serial');
serial().then(() => console.timeEnd('serial')); // 约3秒
console.time('parallel');
parallel().then(() => console.timeEnd('parallel')); // 约1秒
2.8 async/await 错误处理
// 方法1:try/catch(推荐,最接近同步语法)
async function method1() {
try {
const data = await apiCall();
return data;
} catch (err) {
console.error('请求失败:', err.message);
throw err; // 重新抛出,外层仍可捕获
}
}
// 方法2:.catch() 捕获
async function method2() {
const data = await apiCall().catch(err => {
console.error('请求失败:', err.message);
return null; // 返回默认值
});
return data;
}
// 方法3:使用 Promise.allSettled 批量处理,不轻易抛出错误
async function method3() {
const results = await Promise.allSettled([
apiCall('url1'),
apiCall('url2'),
apiCall('url3'),
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(${i} 成功:, result.value);
} else {
console.warn(${i} 失败:, result.reason.message);
}
});
}
三、代码示例
3.1 完整的 async/await 增删改查示例
// 模拟 API 延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const mockApi = {
async getUser(id) {
await delay(300);
return { id, name: 'Alice', email: 'alice@example.com' };
},
async getPosts(userId) {
await delay(500);
return [
{ id: 1, title: 'ES6 新特性', views: 100 },
{ id: 2, title: 'Promise 详解', views: 200 },
];
},
async createPost(userId, title) {
await delay(400);
return { id: Math.random(), userId, title, createdAt: new Date() };
}
};
// 使用 async/await 组合多个 API 调用
async function getUserWithPosts(userId) {
try {
// 两个独立的请求可以并行
const [user, posts] = await Promise.all([
mockApi.getUser(userId),
mockApi.getPosts(userId)
]);
return { user, posts, totalViews: posts.reduce((s, p) => s + p.views, 0) };
} catch (err) {
console.error('加载失败:', err);
return null;
}
}
// 创建文章后刷新列表
async function createAndReload(userId, title) {
const newPost = await mockApi.createPost(userId, title);
console.log('文章创建成功:', newPost);
const { posts } = await getUserWithPosts(userId);
return posts;
}
// 测试
getUserWithPosts(1).then(result => {
console.log('用户信息:', result.user);
console.log('文章列表:', result.posts);
console.log('总浏览量:', result.totalViews);
});
3.2 手写简易 Promise 实现
// 简化版 MyPromise 实现,帮助理解 Promise 原理
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.callbacks = [];
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.callbacks.forEach(cb => cb.onFulfilled(value));
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.value = reason;
this.callbacks.forEach(cb => cb.onRejected(reason));
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = (callback, fallback) => {
try {
const fn = typeof callback === 'function' ? callback : fallback;
resolve(fn(this.value));
} catch (err) {
reject(err);
}
};
if (this.state === 'fulfilled') {
handle(onFulfilled, v => v);
} else if (this.state === 'rejected') {
handle(onRejected, err => { throw err; });
} else {
this.callbacks.push({
onFulfilled: () => handle(onFulfilled, v => v),
onRejected: () => handle(onRejected, err => { throw err; })
});
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => { callback(); return value; },
reason => { callback(); throw reason; }
);
}
}
// 测试
new MyPromise((resolve) => setTimeout(() => resolve(42), 1000))
.then(v => { console.log('得到值:', v); return v * 2; })
.then(v => console.log('翻倍:', v));
四、运行效果
4.1 Node.js 环境运行示例
$ node async-demo.js
得到值: 42
翻倍: 84
用户信息: { id: 1, name: 'Alice', email: 'alice@example.com' }
文章列表: [ { id: 1, title: 'ES6 新特性', views: 100 }, ... ]
总浏览量: 300
serial: 3004ms (串行,3个任务各1秒)
parallel: 1003ms (并行,同时执行,总耗时≈1秒)
4.2 浏览器环境测试
在浏览器控制台粘贴以下代码,观察执行顺序:
async function test() {
console.log('1 - 开始');
await new Promise(r => setTimeout(r, 1000));
console.log('2 - 1秒后');
await new Promise(r => setTimeout(r, 1000));
console.log('3 - 再过1秒(共2秒)');
}
test();
console.log('4 - 异步函数启动后立即执行');
/* 输出顺序:
1 - 开始
- 4 - 异步函数启动后立即执行(主线程未被阻塞)
- 2 - 1秒后
- 3 - 再过1秒(共2秒)
*/
五、常见问题
Q1:async/await 和 Promise 是什么关系?
答: async/await 是 Promise 的语法糖。async 函数自动返回 Promise,await 等待 Promise resolve。实际上所有 async 函数背后的执行机制都是 Promise。以下两者等价:
// 写法1:纯 Promise + then/catch
function getData1() {
return fetch('/api/data')
.then(r => r.json())
.then(data => process(data));
}
// 写法2:async/await(编译后实际等价于写法1)
async function getData2() {
const r = await fetch('/api/data');
const data = await r.json();
return process(data);
}
Q2:forEach 配合 async/await 能正常工作吗?
答: 不能。forEach 不等待 async 回调,会并行发起所有请求。
// 错误写法:forEach 不会等待 await
async function wrong() {
const ids = [1, 2, 3];
const results = [];
ids.forEach(async id => {
const data = await fetchData(id); // 不等待,直接继续
results.push(data);
});
console.log(results); // [] (可能为空,任务还在进行中)
}
// 正确写法:使用 for...of(串行)或 Promise.all(并行)
async function correct_serial() {
const ids = [1, 2, 3];
const results = [];
for (const id of ids) {
const data = await fetchData(id); // 等待每个完成后再下一个
results.push(data);
}
return results;
}
async function correct_parallel() {
const ids = [1, 2, 3];
const results = await Promise.all(ids.map(id => fetchData(id)));
return results;
}
Q3:await 在 Promise.all 内部还是外部?
答: 根据需求选择:
- await 在 Promise.all 外部:等所有任务并行完成后再继续(推荐用于独立任务)。
- await 在 Promise.all 内部(每个单独 await):实际上退化为串行执行,性能差,应避免。
// ✅ 推荐:并行,等所有完成后再往下走
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
console.log('所有任务完成');
// ❌ 错误:虽然用了 Promise.all,但 await 写在内部
const results = await Promise.all([
await taskA(), // 阻塞等待 taskA 完成才开始 taskB
await taskB(),
await taskC(),
]);
Q4:Promise.then/.catch 之后还会继续执行吗?
答: 会的。Promise 链式调用中,每个 .then() 返回新的 Promise,即使前面的 .then() 已经有值,也不会自动终止链。
Promise.resolve(1)
.then(v => {
console.log('第一个 then:', v); // 1
return v + 1;
})
.then(v => {
console.log('第二个 then:', v); // 2
// 不返回,则下一个 then 收到 undefined
})
.then(v => {
console.log('第三个 then:', v); // undefined
});
// 如果想提前终止 Promise 链,有以下方式:
// 1. 抛出异常,进入 .catch()
Promise.resolve(1)
.then(v => { throw new Error('stop'); })
.then(v => console.log('不会执行'))
.catch(err => console.log('在 catch 处理:', err.message));
// 2. 返回一个永不 resolve 的 Promise
const never = () => new Promise(() => {});
Promise.resolve(1)
.then(never)
.then(v => console.log('不会执行'));
Q5:async 函数里 return 值 vs return await 值,有什么区别?
答: 有微妙但重要的区别:
// return value:值直接作为 Promise 的 resolve 值
async function fn1() {
return 42;
}
// return await value:多走一步,先 await 再 return
// 区别在于错误堆栈和调试行为
async function fn2() {
return await 42; // await 一个非 Promise 值,几乎没区别
}
// 真正有区别的情况:
async function parent() {
// return await 可能捕获异常,但 return value 会直接抛出
try {
return await riskyOperation(); // 错误在 parent 上下文中抛出
} catch (e) {
return 'fallback';
}
}
// 推荐写法:减少不必要的 await,减少微任务开销
async function getData() {
const r = await fetch('/api/data');
return r.json(); // 直接 return,调用方 await
}
六、延伸阅读
- MDN - Promise:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise — Promise 官方文档,详细介绍了所有实例方法。
- MDN - async/await:https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises — 异步编程教程,适合入门。
- JavaScript Promise 必知必会(英文):https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html — 经典文章,总结 Promise 的常见坑。
- Jake Archibald - 异步迭代器:https://jakearchibald.com/2017/async-iterators-are-cool/ — 深入讲解 async 迭代器和生成器。
- Node.js 官方文档 - 异步编程:https://nodejs.org/en/guides/event-loop-timers-and-nexttick — 理解 Node.js 事件循环与异步执行模型。
- Promise 迷你书(中文):https://wohugb.github.io/promise/index.html — 通俗易懂的中文 Promise 教程。