JavaScript 异步编程:Callback、Promise、async/await
回调函数名字虽然抽象,但我们可以将其理解为“回头再调用的函数”,代表着我交给你一个函数,在任务完成后可以回头调用。,如果某个任务耗时过长(比如网络请求、文件读取、复杂计算),整个页面就会“卡死”,按钮点不动、动画停滞、用户体验瞬间崩塌。Promise人如其名,代表承诺,即:我承诺无论我的内部程序是否正常运行,会在未来某个时候给你一个结果。本质是Promise函数的语法糖,使Promise函数更具有
一、为什么异步编程在JS中如此重要
在 JavaScript 的世界里,异步编程几乎是呼吸般的存在。 这门语言最初诞生于浏览器,为的是让网页在用户操作的同时还能去加载数据、响应事件、渲染动画——而这一切都运行在单线程之上。
单线程意味着同一时间只能做一件事,如果某个任务耗时过长(比如网络请求、文件读取、复杂计算),整个页面就会“卡死”,按钮点不动、动画停滞、用户体验瞬间崩塌。
异步编程正是为了解决这个问题而生:
-
让 JavaScript 可以先挂起耗时任务,继续处理其他工作,等结果准备好再回来执行。
-
让我们能同时监听用户输入、加载数据、播放动画,而不会互相阻塞。
-
是现代 Web 应用、Node.js 服务、跨平台应用流畅运行的基石。
JS异步的演变:回调函数->Promise->async/await
*JS异步运行机制--事件循环:
JavaScript 的异步执行依赖于事件循环(Event Loop),它协调:
-
同步任务:立即执行
-
微任务:如Promise.then,在当前任务结束后立即执行
-
宏任务:如setTimeout,在下一轮事件循环中执行
这种机制确保异步任务不会阻塞主线程,同时也保证执行顺序的可预测性。
在一次事件循环中,先执行所有同步代码 → 清空微任务队列 → 最后处理宏任务。
实例1:
console.log("同步开始");
setTimeout(() => {
console.log("setTimeout 回调");
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 微任务 1");
})
.then(() => {
console.log("Promise 微任务 2");
});
console.log("同步结束");
输出顺序:
同步开始
同步结束
Promise 微任务 1
Promise 微任务 2
setTimeout 回调
解释:
- 同步代码优先执行:console.log("同步开始") 和 console.log("同步结束") 是同步任务,立即执行。
- 微任务队列:
Promise.resolve().then(...) 会将 .then() 的回调加入微任务队列。
所有同步任务执行完后,事件循环会清空微任务队列。 - 宏任务队列:
setTimeout(..., 0) 是宏任务,它会在下一轮事件循环中执行。所以它最后才输出。
实例2:
async function async1() {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1');
});
console.log('async1 success');
return 'async1 end';
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
输出顺序:
script start
async1 start
promise1
script end
解释:
- console.log('script start') → 同步任务,立即执行
- 调用 async1():
输出 'async1 start'
执行 new Promise(...),输出 'promise1'
遇到 await,暂停后续执行,等待 Promise 解决(但它永远不会):await new Promise(resolve => { console.log('promise1'); }); //这段代码创建了一个 Promise,但没有调用 resolve(),所以这个 Promise 永远处于 pending 状态。 //由于 await 会暂停后续代码执行直到 Promise 被解决(fulfilled 或 rejected),而这个 Promise 永远不会解决,所以: //console.log('async1 success') 永远不会执行 //return 'async1 end' 永远不会发生 //.then(res => console.log(res)) 也永远不会触发
- console.log('script end') → 同步任务,立即执行
二、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 状态 |
语法复杂度 | 低 | 中等 | 低(最直观) |
适用场景 | 简单异步任务 | 中等复杂度任务 | 复杂异步流程 |
更多推荐
所有评论(0)