8-从回调函数到 Promise 再到 async/await:JavaScript 异步编程完整演进之路
从回调函数到 Promise 再到 async/await:JavaScript 异步编程完整演进之路
文章目录
- 从回调函数到 Promise 再到 async/await:JavaScript 异步编程完整演进之路
- 前言
- 一、异步编程的起点:ajax 与回调函数的应用与局限
- 二、Promise:异步编程的进阶方案
-
- 2.1 Promise 的核心认知
- 2.2 Promise 的状态与状态变更
-
- 2.2.1 Promise 的构造与执行器函数
- 2.2.2 Promise的三种状态
- 2.2.3 更改状态的三种方案(成功一种,失败两种)
- 2.2.4 状态只允许更改一次!
- 2.2.5 代码练习:then () 方法的基础使用
- 2.2.6 [then() 方法返回的是一个新 Promise 实例(重点)](https://blog.csdn.net/weixin_39952495/article/details/159649281?sharetype=blogdetail&sharerId=159649281&sharerefer=PC&sharesource=weixin_39952495&spm=1011.2480.3001.8118)
- 2.3 用 Promise 改造 AJAX 示例
- 三、async/await:异步编程的终极优雅方案
- 四、三种方案完整对比
- 五、拓展:Promise 与 jQuery 异步方案的对比与关联
- 总结
前言
在前端开发中,异步编程是绕不开的核心能力 —— 从最早的 AJAX 数据请求,到如今复杂的前后端交互,异步逻辑贯穿所有业务场景。
本文将带你走一遍 JavaScript 异步编程的完整演进之路:从回调函数的痛点与闭包原理,到 Promise 如何解决回调地狱,再到 async/await 如何让异步代码写得像同步一样优雅。
读完你会彻底明白:为什么回调是起点,Promise 是进阶,而 async/await 是现代前端的最终方案。
一、异步编程的起点:ajax 与回调函数的应用与局限
1.1 「ajax 函数」的定义
希望把读取到的文件的响应体的内容输出,就是下面的写法:
function ajax(){
const xhr = new XMLHttpRequest();
xhr.open("get","2-回调函数.html");
xhr.send();
xhr.onload = function(){// 异步回调
console.log(xhr.response)
}
}
ajax();// 调用,
关键步骤:
1-创建xhr对象,这是浏览器发起HTTP请求的 “工具”。XMLHttpRequest是浏览器原生提供的、用于发起 AJAX 请求的核心对象。
2-xhr.open+xhr.send:用于配置请求方法、URL,以及真正发送请求(发起一个GET请求,目标是2-回调函数.html(实际会获取该文件内容))。
3-xhr.onload:异步回调,会在请求完成(无论成功 / 失败)后触发,上例在里面打印了xhr.response来获取响应体内容。
1.2 如果再次调用ajax(),希望弹框怎么办?
function ajax(){
const xhr = new XMLHttpRequest();
xhr.open("get","2-回调函数.html");
xhr.send();
xhr.onload = function(){// 异步回调
console.log(xhr.response)
}
}
ajax();// 调用,希望把读取到的文件的响应体的内容输出,就是上述的写法
ajax();// 调用,希望弹框怎么办?就满足不了了,
也就说这里封装了一个ajax函数,可以获取到响应体数据,但是响应体数据在不同的调用场景下由调用者决定展示方式(输出还是弹框)这样写死了明显不合理,现在调用第一个ajax希望是输出,调用第二个ajax是弹框,所以就不能再直接写死,可以借助回调函数:ajax()调用的时候把一个函数传过去。然后就可以被ajax函数接收和调用,其实就是闭包的一种应用:
1.3 通过回调函数实现灵活处理响应数据
function ajax(cb){
const xhr = new XMLHttpRequest();
xhr.open("get","2-回调函数.html");
xhr.send();
xhr.onload = function(){
cb()
}
}
ajax(function(){
});
当获取到响应体数据以后,调用cb()并且把响应体数据传过去:cb(xhr.response),ajax(function(){ 这里就可以接收数据:ajax(function(res){,然后想输出就输出,想弹框就弹框。
function ajax(cb){
const xhr = new XMLHttpRequest();
xhr.open("get","2-回调函数.html");
xhr.send();
xhr.onload = function(){
cb(xhr.response)
}
}
// 封装 Ajax 请求流程,让使用更简洁。
// 1-创建xhr对象,这是浏览器发起HTTP请求的 “工具”。
// 2-xhr.open+xhr.send:发起一个GET请求,目标是2-回调函数.html(实际会获取该文件内容)。
// 3-xhr.onload:异步回调,请求成功完成后触发。此时调用传入的cb(即外部传的回调函数),并把 xhr.response(请求结果)作为参数传递。
ajax(function(res){// 第一次调用:控制台输出
console.log(res);
});
ajax(function(res){// 第二次调用:弹窗展示
alert(res);
})
// 调用 ajax 发起请求,同时指定 “请求成功后要做的事”(通过回调函数 cb 实现)。
// 第一个ajax调用:传入匿名函数 (res) => { console.log(res) } 作为 cb。当 Ajax 请求完成(xhr.onload 触发),cb(xhr.response) 会执行 → 也就是 console.log(res),把请求结果打印出来。
// 第二个 ajax 调用:传入匿名函数 (res) => { alert(res) } 作为 cb。同样,请求完成后会执行 alert(res),用弹窗显示结果。
定义了一个ajax函数,函数体里有异步行为,然后在函数的外面调用,异步获取到的数据怎么在外面使用?可以调用ajax函数的时候传回调函数,这就是旧的方案:单纯使用回调函数来解决异步编程的问题。这种解决方案有个问题,阅读性差,如果嵌套了多层就很容易看不懂了。
1.4 闭包的应用
闭包的核心特征是:内层函数可以访问外层函数作用域中的变量 / 函数,即使外层函数已经执行完毕。
这段代码中存在闭包的应用,主要体现在 ajax 函数内部的 xhr.onload 回调与外部传入的 cb 函数之间的关系上:
function ajax(cb) {
const xhr = new XMLHttpRequest();
xhr.open("get", "2-回调函数.html");
xhr.send();
xhr.onload = function() { // 内层函数(异步回调)
cb(xhr.response); // 访问外层的 cb 和 xhr
}
}
// 调用时传入的回调函数
ajax(function(res) {
console.log(res);
});
- 外层函数:
ajax(cb),它的作用域中有两个关键变量:cb(外部传入的回调函数)和xhr实例。 - 内层函数:
xhr.onload对应的匿名函数(异步执行,等请求完成后才会触发)。 - 当 ajax 函数执行完毕后,理论上它的作用域应该被销毁,但由于
xhr.onload这个内层函数还在引用cb和xhr,JavaScript 引擎会保留ajax函数的作用域 —— 这就形成了闭包。
闭包在这里的作用:确保 xhr.onload 回调在异步执行时(请求完成后),仍然能访问到 ajax 函数作用域中的 cb(外部传入的回调)和 xhr(获取响应结果 xhr.response)。
简单说:闭包让异步回调能 “记住” 外层 ajax 函数中的变量,否则 xhr.onload 触发时,cb 和 xhr 早就被销毁了,无法执行 cb(xhr.response) 这行代码。
总结:这段代码中,xhr.onload 回调通过闭包 “抓住” 了外层 ajax 函数的 cb 和 xhr 变量,确保异步请求完成后能正确调用外部传入的回调函数并传递结果。这是闭包在异步编程中最常见的应用场景之一 ——让异步操作能访问到定义时的上下文环境。
1.5 回调方案的问题
“嵌套多层就会很难懂”,这是回调函数方案的核心问题,俗称 “回调地狱(Callback Hell)”。举个极端但好懂的例子:如果需求是 “先调用 ajax1 获取结果,再用结果作为参数调用 ajax2,再用 ajax2 结果调用 ajax3”,代码会变成这样:
ajax(function(res1) {
ajax(function(res2) {
ajax(function(res3) {
// ... 一层套一层,极其难维护
});
});
});
可读性直线下降:嵌套层级越深,代码越像 “金字塔”,逻辑flow被打乱。
异常处理麻烦:每一层回调的错误,很难统一捕获、处理。
1.6 总结
这段代码的逻辑是:
1-封装 ajax 函数,用 回调函数 cb 处理 “异步请求完成后的数据使用”。
2-调用时传入不同回调,分别用 console.log、alert 处理结果。
3-但这种方案的缺点是 “多层嵌套会让代码难以阅读、维护” → 这也是后来 Promise、Async/Await 等方案诞生的原因,用来解决回调地狱问题。简单说,这是 “用回调函数解决异步” 的经典写法,能跑通逻辑,但在复杂场景会很棘手。
二、Promise:异步编程的进阶方案
在第一部分我们看到,传统回调函数虽然能解决异步数据处理问题,但多层嵌套会带来回调地狱,代码可读性和可维护性急剧下降。而 Promise 正是为了解决这类问题而生的异步编程新方案。
2.1 Promise 的核心认知
从两个维度快速理解 Promise:
- 语法层面:Promise 是 JavaScript 原生提供的构造函数,可以通过 new Promise() 实例化一个 promise 对象。
- 功能层面:promise 对象的核心作用是封装一个异步操作,并在未来某个时刻获取该操作的成功结果或失败原因,让异步逻辑可以用更线性、更清晰的方式组织。
关于 Promise 的详细语法、状态机制、then/catch 等 API 用法,我已经在另一篇文章中做了完整讲解:
Promise 从 A+ 规范到 JS 实战:完整原理与 API 全解析
2.2 Promise 的状态与状态变更
Promise 的核心是状态管理,理解状态才能真正掌握异步流程。
2.2.1 Promise 的构造与执行器函数
Promise 是 JavaScript 内置的构造函数,我们通过 new Promise() 来创建实例,这个过程中会传入一个执行器函数,它是理解 Promise 运行机制的第一步。
-
执行器函数是同步执行的
当我们调用new Promise()时,传入的执行器函数会立即同步执行,不会进入异步队列。// 执行器函数会在 new Promise 时立刻执行 new Promise(function(){ console.log("1-执行器函数,执行器函数不允许省略"); }); console.log("2-over"); // 先输出 1,再输出 2-over这一点很关键:执行器函数是同步的,而
.then()回调才是异步的,很多初学者容易混淆。 -
Promise 实例的核心属性
new Promise() 会返回一个 Promise 实例,它有两个最重要的内部属性:state:表示 Promise 的当前状态(pending/fulfilled/rejected)value:存储 Promise 的结果数据(成功时是 value,失败时是 reason)const p1 = new Promise(function(){ // 未调用 resolve/reject 时,状态保持为 pending }); console.log(p1); // 状态为 pending,值为 undefined
2.2.2 Promise的三种状态
promise的状态分为三种,分别是:pending(初始状态)、fulfilled(成功的状态)、rejected(失败的状态)
const p1 = new Promise(()=>{});
console.log(p1);// 状态为:pending,值为undefined,什么情况下状态会成功或者失败呢?
2.2.3 更改状态的三种方案(成功一种,失败两种)
只有三种方式可以将 Promise 状态从 pending 变更为最终态:
方案 1:调用 resolve() → 变为 fulfilled(成功)
执行器函数接收 resolve 作为参数,调用它会将状态改为成功,并传入成功结果。
const p = new Promise(function(resolve, reject){
resolve(1); // 调用 resolve,将状态从pending变为fulfilled,成功值为1
});
console.log(p); // Promise {<fulfilled>: 1}
方案 2:调用 reject() → 变为 rejected(失败)
执行器函数接收 reject 作为参数,调用它会将状态改为失败,并传入失败原因。
const p = new Promise(function(resolve, reject){
reject(2); // 调用reject,将状态从pending变为rejected,失败的值为2
});
console.log(p); // Promise {<rejected>: 2}
方案 3:执行器内抛出异常 → 自动变为 rejected(失败)
如果执行器函数中出现代码错误(如类型错误)或手动 throw 异常,Promise 会自动变为 rejected。
// 自动类型错误
const p = new Promise(function(){
let a = 1;
a(); // 类型错误,将状态从pending变为rejected,失败的值为异常TypeError 信息
});
console.log(p); // 注意!执行器函数中有异常,后面的程序还会继续执行,打印p
// Promise {<rejected>: TypeError: a is not a function}
// 手动抛出异常
const p = new Promise(function(){
throw "异常"; // 将状态从pending变为rejected,失败值为字符串 "异常"
});
console.log(p); // Promise {<rejected>: "异常"}
2.2.4 状态只允许更改一次!
Promise 状态只能变更一次,一旦从 pending 变为 fulfilled 或 rejected,就会永久锁定,后续的状态修改操作都会被忽略。
const p = new Promise(function(resolve,reject){
resolve(1); // 先变为 fulfilled
reject(2); // 无效,状态已锁定
});
console.log(p);// fullfiled 1 所以打印的就是成功,值为1。
// 已经变成成功了,下面再变为失败就变不了了。即使手动抛异常也无济于事。
const p = new Promise(function(resolve,reject){
resolve(1);
reject(2);
throw "异常"; // 无效
});
console.log(p);// fullfiled 1 所以打印的就是成功,值为1。
// ⚠️ 注意:状态变更后,执行器函数内的剩余代码仍会继续执行,只是无法再改变 Promise 状态:
const p = new Promise(function(resolve,reject){
resolve(1);
reject(2);
console.log(111111)// // 会正常输出
throw "异常"; // 抛出异常,但状态已锁定
console.log(22222)// 不会执行,因为异常中断了后续代码
});
console.log(p);// fullfiled 1
2.2.5 代码练习:then () 方法的基础使用
.then() 是 Promise 原型上的方法,用于监听状态变更后的结果,它接收两个函数参数:
第一个函数:成功回调,当 Promise 变为 fulfilled 时异步执行,参数为成功值 value
第二个函数:失败回调,当 Promise 变为 rejected 时异步执行,参数为失败原因 reason
const p = new Promise(function(resolve,reject){
// 未调用 resolve/reject 时,状态为 pending,值为 undefined
});
console.log(p);
==================================================================
const p = new Promise(function(resolve,reject){
});
console.log(p);// 发现原型上有个then方法,所以就可以调用原型上的方法:
// p.then()可以接收两个参数,参数的类型均为函数。
// 第一个函数称为成功回调(当p的状态由pending-->fulfilled后异步执行),接收的参数是成功值
// 第二个函数称为失败回调(当p的状态由pending-->rejected后异步执行),接收的参数是失败值
p.then(function(value){// 成功值一般叫value
console.log("成功回调",value);
},function(reason){// 失败值一般叫reason
console.log("失败回调",reason)
})
=================================================================
const p = new Promise(function(resolve,reject){
resolve(100);
});
p.then(function(value){
console.log("成功回调",value);// 输出成功回调 100
},function(reason){
console.log("失败回调",reason)
})
=================================================================
const p = new Promise(function(resolve,reject){
resolve(100);
reject(200);
});
p.then(function(value){
console.log("成功回调",value);// 还是输出成功回调 100,因为状态只能更改一次
},function(reason){
console.log("失败回调",reason)
})
=================================================================
const p = new Promise(function(resolve,reject){
reject(200);
throw "异常";
});
p.then(function(value){
console.log("成功回调",value);
},function(reason){
console.log("失败回调",reason)// 输出失败回调 200
})
=================================================================
const p = new Promise(function(resolve,reject){
throw "异常";
});
p.then(function(value){
console.log("成功回调",value);
},function(reason){
console.log("失败回调",reason)// 输出失败回调 异常
})
通过 .then() 我们可以监听 Promise 状态变更后的结果,这也是后续异步流程处理的基础。
2.2.6 then() 方法返回的是一个新 Promise 实例(重点)
2.3 用 Promise 改造 AJAX 示例
我们可以把第一部分的 AJAX 异步逻辑,用 Promise 重新封装,直观感受它和传统回调的差异:
// 用 Promise 封装 AJAX 请求
function ajaxPromise() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("get", "2-回调函数.html");
xhr.send();
xhr.onload = function() {
// 请求完成后,将响应体传递给 resolve,标记异步操作成功
resolve(xhr.response);
};
// 增加错误处理,标记异步操作失败
xhr.onerror = function() {
reject(new Error("网络请求失败"));
};
});
}
// 调用时用 then 接收成功结果,catch 处理错误
ajaxPromise()
.then(function(res) {
console.log("成功获取数据:", res);
})
.catch(function(err) {
console.error("请求出错:", err);
});
核心变化:
- 不再需要在调用时传入回调函数,而是通过
then()链式调用处理结果。 - resolve(xhr.response) 会把结果传递给
then的回调参数res,和之前cb(xhr.response)的作用一致。 - 多层异步操作时,可以通过链式
then避免嵌套,从根本上解决 “回调地狱” 问题。
三、async/await:异步编程的终极优雅方案
3.1 为什么还需要 async/await?
虽然 Promise 解决了回调地狱,但依然是基于回调 + 链式调用的写法:依然要写 .then()、.catch(),流程控制、条件判断、循环等写起来还是不够直观。
async/await 本质:是 Promise 的语法糖;让异步代码写起来和同步代码几乎一模一样;可读性、可维护性达到目前最高水平。
3.2 基本用法
// 函数前面加 async,就表示这是一个异步函数
async function getDate() {
try {
// await 后面跟一个 Promise
const res = await ajaxPromise();
console.log(res);
} catch (err) {
// 统一捕获错误
console.error(err);
}
}
getData();
特点:代码线性、扁平,没有嵌套;用 try/catch 统一处理异常,非常符合同步代码习惯;阅读和调试都极其方便。
3.3 用 async/await 改造 AJAX 示例
直接基于我们前面封装好的 ajaxPromise:
// 前面封装好的 Promise 版 AJAX
function ajaxPromise() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("get", "2-回调函数.html");
xhr.send();
xhr.onload = () => resolve(xhr.response);
xhr.onerror = () => reject(new Error("请求失败"));
});
}
// async/await 调用
async function fetchData() {
try {
const result = await ajaxPromise();
console.log("数据:", result);
} catch (error) {
console.error("出错:", error);
}
}
fetchData();
3.4 串行多个请求(对比最明显)
回调地狱:
ajax(res1 => {
ajax(res2 => {
ajax(res3 => {
console.log(res3)
})
})
})
Promise 链式:
ajaxPromise()
.then(() => ajaxPromise())
.then(() => ajaxPromise())
.then(res => console.log(res))
async/await:一眼就能看出:代码从上到下顺序执行,完全像同步代码。
async function getAll() {
await ajaxPromise();
await ajaxPromise();
const res = await ajaxPromise();
console.log(res);
}
四、三种方案完整对比
| 方案 | 写法特点 | 可读性 | 错误处理 | 适用场景 |
|---|---|---|---|---|
| 传统回调 | 嵌套多层(回调地狱),需手动传递回调函数 | 差(层级越深越难维护) | 分散在各层回调中,无统一处理方式 | 简单异步场景、异步编程入门学习 |
| Promise | 链式调用(.then()/.catch()),无嵌套 | 较好(线性流程,逻辑清晰) | 统一通过 .catch() 捕获所有环节错误 | 中大型项目、复杂异步流程(串行/并行请求) |
| async/await | 类同步写法,无嵌套、无链式调用 | 极好(与同步代码逻辑一致,易读易调试) | 通过 try/catch 统一捕获,符合同步编程习惯 | 现代前端开发、主流首选方案,适配各类异步场景 |
一句话总结:
回调函数:异步编程的基础,但容易写出回调地狱。
Promise:解决回调地狱,让异步流程线性化。async/await:在 Promise 基础上,让异步代码写得和同步代码一样优雅。
五、拓展:Promise 与 jQuery 异步方案的对比与关联
在 Promise 成为 ES6 语言标准之前,jQuery 早已在前端领域广泛使用,并通过 Deferred 对象与 $.ajax 提供了一套成熟的异步解决方案。我们可以从历史渊源、核心共性与关键差异三个维度,理解两者的关系与价值。
5.1 历史渊源:Promise 思想的早期实践
- jQuery 在 2011 年推出的 Deferred 对象,是Promise 设计思想的重要先驱,它率先提出了「状态管理 + 链式回调」的异步模式,解决了早期回调嵌套问题。
- 后来 ES6 原生 Promise 规范(Promises/A+)正是吸收了包括 jQuery Deferred 在内的多种异步方案的优点,最终成为 JavaScript 语言级标准。
- jQuery 3.0 之后也做了兼容调整,让 Deferred 对象符合 Promises/A+ 规范,可以和原生 Promise 混用。
5.2 核心共性:解决异步问题的思路一致
-
状态管理思想
- jQuery Deferred:pending → resolved/rejected
- 原生 Promise:pending → fulfilled/rejected
- 两者都用「状态流转」来标记异步操作的进展,只有状态改变后才会触发回调。
-
链式调用避免回调地狱
- jQuery:$.ajax().done().fail().always()
- Promise:.then().catch().finally()
- 都通过链式写法,把多层嵌套展平为线性流程,提升可读性。
-
分离「发起请求」与「处理结果」
两者都将异步操作的发起(发送请求)和结果处理(成功 / 失败回调)解耦,让代码更模块化、可复用。
5.3 关键差异:库实现 vs 语言标准
- 出身与定位不同:jQuery 的 Deferred 是第三方库内部实现,完全服务于 jQuery 生态,依赖 jQuery 环境才能使用;而原生 Promise 是 ES6 语言级标准,不依赖任何库,在所有现代 JS 环境(浏览器、Node.js 等)中都能直接运行。
- 通用性不同:jQuery 的异步方案主要绑定在 AJAX 请求和 DOM 操作场景,适用范围较窄;原生 Promise 则是通用异步抽象,能封装所有异步场景(定时器、文件读写、网络请求、Web Worker 等)。
- 规范与兼容性不同:早期 jQuery Deferred 是自定义实现,并不符合 Promises/A+ 规范,直到 jQuery 3.0 才做了兼容调整;而原生 Promise 从诞生起就严格遵循 Promises/A+ 规范,是 async/await 语法的底层基础,生态兼容性更稳定。
- 错误处理与生态地位不同:jQuery 用 .fail() 捕获错误、.always() 做收尾,错误处理分散在链式调用中;原生 Promise 用 .catch() 统一捕获整条链上的错误,.finally() 做兜底,更健壮。如今 jQuery 已逐渐衰落,而原生 Promise 是现代前端异步编程的基石,是所有主流框架的底层依赖。
5.4 学习启示:从历史看技术演进
jQuery Deferred 是「过渡方案」:它在浏览器原生能力不足的年代,用库的方式解决了异步痛点,但始终受限于 jQuery 生态。
原生 Promise 是「最终标准」:它将优秀的异步思想沉淀为语言能力,不仅解决了回调地狱,还为更优雅的 async/await 铺平了道路。
对我们学习的启发:技术方案总是从「库级封装」走向「语言级标准」,理解这一点,能帮我们更好地判断哪些方案是长期值得投入的。
总结
本文从最基础的 AJAX 异步请求出发,完整梳理了 JavaScript 异步编程的演进路径:
- 回调函数:作为异步编程的起点,通过闭包实现了异步数据的灵活处理,但多层嵌套会导致 “回调地狱”,可读性与可维护性极差。
- Promise:作为进阶方案,用状态管理与链式调用解决了回调地狱问题,将异步流程线性化,为现代异步编程奠定了基础。
async/await:作为终极优雅方案,在 Promise 基础上用类同步的写法,让异步代码的可读性与调试体验达到了最优。- 拓展思考:jQuery 的 Deferred 是 Promise 思想的早期实践,它证明了 “状态 + 链式” 异步方案的价值,但最终被语言标准的原生 Promise 所取代,体现了技术从 “库级封装” 走向 “语言级标准” 的演进规律。
通过这一路径,我们能清晰看到:异步编程的本质,是在不断追求更清晰、更健壮、更接近人类思维的代码表达方式。从回调到 Promise,再到 async/await,每一次演进都是为了让开发者能更轻松地驾驭复杂的异步逻辑,这也是前端工程化与现代化的核心体现之一。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)