前言
作为前端开发者,我们每天都要和“异步”打交道——比如等待接口返回数据、延迟执行一段代码、处理用户交互后的回调。而JavaScript默认是单线程运行的特性,让异步处理成为了入门必学的重点,也让很多新手栽在了“回调地狱”里 。今天我们就结合具体代码,从异步痛点出发,一步步解锁Promise的用法,轻松搞定异步编程~
一、先搞懂:JS为什么需要异步处理?
首先要明确一个核心知识点:JS默认是单线程运行的(V8引擎默认只开启一个主线程执行JS代码)。
为什么设计成单线程?因为JS最初是为浏览器设计的脚本语言,单线程可以节约设备性能——想象一下,如果JS同时执行多个操作,比如一边修改DOM、一边请求数据,很容易导致页面混乱,单线程能避免这种冲突。
但单线程也有个问题:如果遇到耗时操作(比如setTimeout、接口请求),如果一直等待它执行完,后面的代码就会被卡住(也就是“阻塞”)。所以V8引擎会做一件聪明的事:将耗时的异步代码挂起,先执行不耗时的同步代码,等异步操作完成后,再回头执行挂起的代码。
举个简单例子:
let a =1
setTimeout(()=>{
a =2 // 异步代码,1秒后执行
},1000)
console.log(a); // 同步代码,优先执行
执行结果:先打印出1,1秒后才会把a改成2。因为setTimeout是异步操作,被V8挂起,先执行同步的console.log。
二、异步的“噩梦”:回调地狱
早期处理异步,我们全靠“回调函数”——就是把一个函数作为参数,传给另一个异步函数,等异步操作完成后,再调用这个回调函数。但如果有多个异步操作需要顺序执行,就会出现“函数嵌套函数”的情况,这就是回调地狱。
典型的回调嵌套:
let a = 1
function foo() {
setTimeout(() => {
a = 2
console.log('foo', a);
bar() // 第一个异步完成后,调用第二个异步
}, 1000)
}
function bar() {
setTimeout(() => {
a = 3
console.log('bar', a);
baz() // 第二个异步完成后,调用第三个异步
}, 2000)
}
function baz() {
console.log('baz', a);
}
foo()
这段代码的逻辑是:foo执行→1秒后修改a为2→调用bar→2秒后修改a为3→调用baz。看似正常,但如果异步操作再多几层,代码就会像“金字塔”一样越嵌套越深,可读性差、维护困难,排查bug时更是让人头大 。
这就是回调地狱的痛点:嵌套过深、代码混乱、难以维护。而Promise的出现,就是为了解决这个问题!
三、Promise登场:优雅解决异步问题
Promise是ES6引入的异步编程解决方案,它的核心作用是:将异步操作的“结果”和“处理逻辑”分离,用链式调用替代嵌套回调,让异步代码更简洁、更易读。
1. Promise的基础概念
Promise本质是一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。它有三种状态,且状态一旦改变,就不会再变:
-
🔵 等待态(Pending):初始状态,异步操作还没完成
-
🟢 成功态(Fulfilled):异步操作完成,返回成功结果
-
🔴 失败态(Rejected):异步操作失败,返回错误信息
Promise的构造函数接收一个函数作为参数,这个函数有两个内置参数:resolve(成功时调用)和reject(失败时调用),对应两种状态的切换。
2.Promise的基本用法
Promise构造函数简化版,再补充完整逻辑:
class Promise {
constructor(fn) {
// 初始状态:等待态
this.status = 'pending';
// 成功的结果
this.successResult = null;
// 失败的原因
this.failReason = null;
// resolve函数:将状态改为成功态,保存成功结果
function resolve(result) {
// 状态一旦改变,就不能再修改
if (this.status !== 'pending') return;
this.status = 'fulfilled';
this.successResult = result;
}
// reject函数:将状态改为失败态,保存失败原因
function reject(reason) {
if (this.status !== 'pending') return;
this.status = 'rejected';
this.failReason = reason;
}
// 执行传入的函数,传入resolve和reject(绑定this,避免指向错误)
fn(resolve.bind(this), reject.bind(this));
}
// 补充then方法(简化版)
then(onFulfilled, onRejected) {
// 如果状态是成功态,执行onFulfilled,传入成功结果
if (this.status === 'fulfilled') {
onFulfilled(this.successResult);
}
// 如果状态是失败态,执行onRejected,传入失败原因
if (this.status === 'rejected') {
onRejected(this.failReason);
}
}
}
// 测试一下
new Promise((resolve, reject) => {
resolve('成功啦~');
}).then((res) => {
console.log(res); // 打印:成功啦~
})
创建Promise实例时,需要传入一个函数(通常是异步函数),这个函数接收两个参数:resolve和reject,分别对应“成功时调用”和“失败时调用”。下面是一个封装定时器的Promise:
// qc()函数:3秒后执行,调用reject(注意:你写的“返回成功状态”是笔误哦)
function qc() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('闹钟响了');
reject('又被关了') // 调用reject,状态变为失败态
}, 3000)
})
}
// sy()函数:2秒后执行,调用reject
function sy() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('该刷牙了');
reject('该洗脸了') // 调用reject,状态变为失败态
}, 2000)
})
}
// work()函数:1秒后执行(同步函数,无Promise)
function work() {
setTimeout(() => {
console.log('出门了');
}, 1000)
}
3. Promise的链式调用:.then() 和 .catch()
Promise对象有两个核心方法:.then() 和 .catch(),用于处理异步结果,实现链式调用,彻底告别嵌套。
-
.then(res = > {}):接收异步操作的成功结果
-
.catch(err = > {}):接收异步操作的错误信息(只有状态为Rejected时执行)
重点:.then() 本身也会返回一个Promise对象,所以我们可以一直链式调用 .then(),让多个异步操作按顺序执行。
看你的链式调用代码(解析执行过程):
qc()
.then(() => { // 若qc()成功,执行这里
return sy() // 返回sy()的Promise,让下一个.then()等待sy()完成
})
.then(() => { // 若sy()成功,执行这里)
work()
})
.catch((err) => { // 捕捉整个链式中所有的失败信息
console.log('catch', err);
})
执行结果解析:
-
执行qc(),立即返回一个Promise对象,初始状态为「等待态」;
-
3秒后,qc()内部调用reject('又被关了'),Promise状态变为「失败态」;
-
因为状态是失败态,所以后面的两个.then()都不会执行,直接跳转到.catch();
-
最终打印:
闹钟响了→catch 又被关了。
小修改:让链式调用正常执行
如果想让.then()正常执行,只需把reject改成resolve(切换为成功态),比如:
function qc() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('闹钟响了');
resolve('起床啦') // 改为resolve,状态变为成功态
}, 3000)
})
}
function sy() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('该刷牙了');
resolve('刷牙完成') // 改为resolve
}, 2000)
})
}
// 重新执行链式调用
qc()
.then((res) => {
console.log('qc成功:', res); // 打印:qc成功:起床啦
return sy()
})
.then((res) => {
console.log('sy成功:', res); // 打印:sy成功:刷牙完成
work() // 执行出门函数
})
.catch((err) => {
console.log('catch', err);
})
此时执行结果:3秒后打印「闹钟响了」→「qc成功:起床啦」→ 等待2秒打印「该刷牙了」→「sy成功:刷牙完成」→ 等待1秒打印「出门了」,完美实现异步顺序执行 。
四、总结:Promise的核心优势
-
✅ 解决回调地狱:用链式调用替代嵌套,代码更简洁、易维护;
-
✅ 状态可控:三种状态一旦确定,不会再改变,避免异步结果混乱;
-
✅ 结果分离:异步操作的“执行”和“结果处理”分离,逻辑更清晰;
-
✅ 链式调用:支持多个异步操作顺序执行,无需嵌套。
最后提醒一句:Promise虽然解决了回调地狱,但如果有多个并行的异步操作,或者更复杂的异步场景,还可以结合async/await,让异步代码看起来更像同步代码。
希望这篇文章能帮你搞懂Promise,从此告别异步烦恼,轻松拿捏JS异步编程!