JavaScript 异步编程:Callback、Promise、async/await
一、为什么异步编程在JS中如此重要
在 JavaScript 的世界里,异步编程几乎是呼吸般的存在。 这门语言最初诞生于浏览器,为的是让网页在用户操作的同时还能去加载数据、响应事件、渲染动画——而这一切都运行在单线程之上。
单线程意味着同一时间只能做一件事,如果某个任务耗时过长(比如网络请求、文件读取、复杂计算),整个页面就会“卡死”,按钮点不动、动画停滞、用户体验瞬间崩塌。
异步编程正是为了解决这个问题而生:
让 JavaScript 可以先挂起耗时任务,继续处理其他工作,等结果准备好再回来执行。
让我们能同时监听用户输入、加载数据、播放动画,而不会互相阻塞。
是现代 Web 应用、Node.js 服务、跨平台应用流畅运行的基石。
二、Callback回调函数
回调函数定义:被作为参数传递的函数就成为回调函数。
回调函数名字虽然抽象,但我们可以将其理解为“回头再调用的函数”,代表着我交给你一个函数,在任务完成后可以回头调用。实现了简单的异步编程。
优点:
- 回调函数简单直接,易于实现
- 让指定函数在恰当时机,以恰当触发条件被触发
- 让函数更灵活,可以按照实际需要调整函数
- 提高程序效率,如我需要在指定时间触发某函数,在不使用回调函数的情况,我需要在函数内不断查询当前时间,不仅效率低下还会使线程堵塞。而使用回调函数就可以查询当前时间后,计算还剩多少时间,使用setTimeOut()函数触发,在触发指定函数前将线程让给其他程序。
简单来说,触发程序就像餐馆上餐,这份餐就是程序运行的结果。而以往我们需要不停询问厨房是否做好,使用回调函数就像厨房给了我们一份呼号机,出餐后通知我们并上餐。
缺点:
- 如果我们需要使用多个回调函数,需要在每一层2函数中层层嵌套,使得代码难以阅读和维护,运行结果处理分散,形成回调地狱。
// 回调地狱:层层嵌套的回调(Callback Hell)
function callbackHell(userId, done) {readConfig((err, config) => {if (err) return done(err);connectDb(config.db, (err, conn) => {if (err) return done(err);findUser(conn, userId, (err, user) => {if (err) return done(err);fetchProfile(user.token, (err, profile) => {if (err) return done(err);transformData(profile, (err, report) => {if (err) return done(err);saveReport(config.output, report, (err) => {if (err) return done(err);done(null, "完成:报告已生成");});});});});});});
}// ——— 模拟的异步函数们 ———
function readConfig(cb) {setTimeout(() => {console.log("[readConfig]");// 模拟成功cb(null, { db: "db://example", output: "report.txt" });}, 100);
}function connectDb(uri, cb) {setTimeout(() => {console.log("[connectDb]", uri);cb(null, { uri, connected: true });}, 120);
}function findUser(conn, userId, cb) {setTimeout(() => {console.log("[findUser]", userId);// 模拟可能失败if (userId == null) return cb(new Error("缺少 userId"));cb(null, { id: userId, token: "token-abc" });}, 80);
}function fetchProfile(token, cb) {setTimeout(() => {console.log("[fetchProfile]", token);cb(null, { name: "Alice", age: 28 });}, 150);
}function transformData(profile, cb) {setTimeout(() => {console.log("[transformData]");try {const report = `User: ${profile.name}, Age: ${profile.age}`;cb(null, report);} catch (e) {cb(e);}}, 60);
}function saveReport(path, content, cb) {setTimeout(() => {console.log("[saveReport]", path, "->", content);cb(null);}, 70);
}// 运行示例
callbackHell(42, (err, msg) => {if (err) {console.error("失败:", err.message);} else {console.log("成功:", msg);}
});
可以看到,如果运行出错,我们难以在层层嵌套中debug。
为了解决回调地狱,我们在ES6中引入了Promise。
三、Promise
Promise人如其名,代表承诺,即:我承诺无论我的内部程序是否正常运行,会在未来某个时候给你一个结果。
设计动机:解决回调地狱,统一错误处理。
核心概念:状态(pending-处理中/fullfilled-已解决/rejected-已拒绝)
基本用法:new Promise(创建promise对象)、resolve(传递并包装成功结果)、rejected(传递错误结果)
//在函数内部:
resolve('这里内容会被作为成功结果传出')reject('这里结果会被作为错误结果传出')
链式调用:
- .then(res=>{}):
接受上一级的成功结果,res为接受的参数,同时若有需要再向下调起下一级函数。 - .catch(err=>{}):
接受所有位置的错误结果,err为接收到的错误,同时自定义发生错误后程序怎样执行。 - .finally(()=>{}):
不接收任何参数,无论程序运行成功与否都会运行,适合做收尾工作而不是处理结果:如关闭连接,隐藏元素等。
使用Promise重写回调地狱(promiseFlow即为callBackHell的重置):
function promiseFlow(userId) {return readConfig().then(config => {return connectDb(config.db).then(conn => ({ config, conn }));}).then(({ config, conn }) => {return findUser(conn, userId).then(user => ({ config, user }));}).then(({ config, user }) => {return fetchProfile(user.token).then(profile => ({ config, profile }));}).then(({ config, profile }) => {return transformData(profile).then(report => ({ config, report }));}).then(({ config, report }) => {return saveReport(config.output, report);});
}function readConfig() {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[readConfig]");resolve({ db: "db://example", output: "report.txt" });}, 100);});
}function connectDb(uri) {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[connectDb]", uri);resolve({ uri, connected: true });}, 120);});
}function findUser(conn, userId) {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[findUser]", userId);if (userId == null) return reject(new Error("缺少 userId"));resolve({ id: userId, token: "token-abc" });}, 80);});
}function fetchProfile(token) {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[fetchProfile]", token);resolve({ name: "Alice", age: 28 });}, 150);});
}function transformData(profile) {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[transformData]");try {const report = `User: ${profile.name}, Age: ${profile.age}`;resolve(report);} catch (e) {reject(e);}}, 60);});
}function saveReport(path, content) {return new Promise((resolve, reject) => {setTimeout(() => {console.log("[saveReport]", path, "->", content);resolve();}, 70);});
}// 运行
promiseFlow(42).then(() => {console.log("成功:报告已生成");}).catch(err => {console.error("失败:", err.message);});
四、async/await语法糖
本质是Promise函数的语法糖,使Promise函数更具有可读性,结构更接近同步代码。
用法:async返回Promise函数,await等待Promise结果(await只能写在async内部)。
错误处理:try/catch,try中如果出现了错误,catch就会捕捉。
// async/await 版本的流程
async function asyncFlow(userId) {try {const config = await readConfig();const conn = await connectDb(config.db);const user = await findUser(conn, userId);const profile = await fetchProfile(user.token);const report = await transformData(profile);await saveReport(config.output, report);console.log("成功:报告已生成");} catch (err) {console.error("失败:", err.message);}
}//函数定义与Promise写法相同// 运行
asyncFlow(42);
其中,await意思是:需要等待来得到结果,得到结果后就会告诉函数体。
改进:
- 结构线性,看起来像同步代码,顺序一目了然。
- 错误集中处理,包裹整个流程。
- 变量作用域更加清晰。
五、对比
演化路线:
回调 → 最原始的异步方式,但容易混乱。
Promise → 解决回调地狱,提供链式调用和状态管理。
async/await → 在 Promise 基础上进一步简化,让异步代码像同步一样易读。
特性 | 回调函数 Callback | Promise | async/await |
---|---|---|---|
可读性 | 差(嵌套多) | 中等 | 高 |
错误处理 | 分散在回调中 | .catch 统一 | try...catch 统一 |
状态管理 | 无 | 有状态(pending/fulfilled/rejected) | 基于 Promise 状态 |
语法复杂度 | 低 | 中等 | 低(最直观) |
适用场景 | 简单异步任务 | 中等复杂度任务 | 复杂异步流程 |