🐟什么是 Promise
想象你去一家餐厅点餐。你点完餐之后,服务员不会马上把食物给你,而是给你一个 “小纸条”(就像 Promise),这个 “小纸条” 代表着餐厅对你的一个承诺:他们会在食物做好之后给你。在食物还没做好的时候,这个承诺就处于 “进行中” 的状态。
🐟Promise的三种状态
进行中(pending):就像你刚点完餐,厨师还在做菜,这个时候你的这个 “小纸条”(Promise)就处于等待的状态,也就是 “进行中”。已完成(fulfilled):当厨师把菜做好了,服务员把菜端到你面前,这个时候 “小纸条” 的承诺就完成了,就像 Promise 的 “已完成” 状态。而且会带着做好的菜(相当于 Promise 成功后的返回值)。已拒绝(rejected):要是厨师在做菜的时候发现材料不够,做不了你点的菜,这时候就会通过服务员告诉你做不了,这个 “小纸条”(Promise)就处于 “已拒绝” 状态,同时会告诉你为什么做不了(相当于 Promise 失败后的错误原因)。
🐟为什么要用promise??
在 JavaScript 中,异步操作非常常见,例如读取文件、发起网络请求等。在 Promise 出现之前,通常使用回调函数来处理异步操作的结果。当有多个异步操作需要顺序执行时,就会出现回调函数嵌套回调函数的情况,这被称为 “回调地狱”。promise的出现就是为了解决回调地狱问题!
首先我们先要了解异步和回调,回调地狱
- 异步:需要耗时执行的代码
- 回调:一种函数,它作为参数传递给另一个函数,在该另一个函数执行过程中的某个特定时机(比如异步操作完成后或某个事件触发时)被调用,以此来处理相应的结果或执行特定的后续操作。
- 回调地狱: 嵌套过深,一旦出现问题,很难排查,维护难度太大
我们来看几段代码帮助我们理解一下这几个概念
var data = null //全局定义了一个data
function a() {
setTimeout(function () {
data = 'hello'
}, 1000)
}
function b() {
console.log(data);
}
a()
b()
首先定义了两个函数 a 和 b。函数 a 中使用 setTimeout 设置了一个延迟为 1000 毫秒的操作,在这个操作的回调函数中将变量 data 赋值为 'hello',setTimeout 是异步操作,所以当 b 函数被调用时,setTimeout 中的赋值操作还没有执行,因此 console.log(data) 会输出 null。
但其实我们是想要输出hello的,在promise还没有被发明前,聪明的前辈们想到了一个办法去解决它,就是使用回调
function a(callback) {
setTimeout(function () {
const data = 'hello';
callback(data);
}, 1000);
}
function b(data) {
console.log(data);
}
a(b);
函数a接受一个回调函数作为参数。当setTimeout中的异步操作完成后,调用这个回调函数并将数据传递给它。然后在调用a函数时,将b函数作为回调传递进去,这样当异步操作完成后,数据会被传递给b函数进行处理,即输出数据hello。
这样看起来回调还是很优雅的,但是他其实很不优雅,一旦嵌套的函数过多,就会造成回调地狱 我们先来写四个函数的情况看一看,
function func1(callback2, callback3, callback4) {
console.log('Function 1');
callback2(callback3, callback4);
}
function func2(callback3, callback4) {
console.log('Function 2');
callback3(callback4);
}
function func3(callback4) {
console.log('Function 3');
callback4();
}
function func4() {
console.log('Function 4');
}
func1(func2, func3, func4);
是不是已经有点头晕了,如果是10个,20个,甚至是100个,是不是想想就很恐怖了,有种如果改了里面一行代码,就出现一片红色海洋的感觉了。他就像串联了一百个灯泡,如果突然一百个灯泡都没亮了,你能知道哪几个出了问题吗,是不是想想就恐怖,怪不得叫回调地狱呢
🐟引入promise去解决异步问题
我们通过结婚这个案例去讲解promise
function date() {
setTimeout(() => {
console.log('你相亲了');
},3000)
}
function marry() {
setTimeout(() => {
console.log('你结婚了');
},1000)
}
date()
marry()
你花了三天时间去相亲,然后花了一天时间结婚了(不要在意时间哈哈哈,就当是闪婚啦),我们定义了这两个函数,然后分别去调用,先调用date(),然后再是marry(),我们来看看结果
我的发,居然是先结婚,再相亲,这太逆天了,这是因为date()和marry()函数分别使用setTimeout设置了异步操作。但是,由于 JavaScript 的异步执行机制,这些异步操作的执行顺序是不确定的,不能保证先执行date()函数中的异步操作,再执行marry()函数中的异步操作。可以用回调解决,当然我们这里不是用它,而是用promise,首先你想要结婚,肯定得在相亲的时候,使出浑身解数,许下很多承诺,结婚了我要送你大砖戒💍,住大房子🏠,这样别人才会和你结婚
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲了');
resolve()
}, 2000)
})
🐟resolve
🐟改变 Promise 状态
这个操作就等同于你在date函数中,返回了一个promsie实例对象,其中加了一行resolve(),这个是什么呢,在这段代码中,resolve是一个函数,它是Promise构造函数的参数(一个执行函数)内部的一部分。resolve的主要作用是将Promise的状态从pending(进行中)转换为fulfilled(已成功)。
就好比你安排了一个任务(这里是setTimeout模拟的等待 2 秒后 “相亲” 这个任务),当这个任务顺利完成(相亲这个动作执行了,也就是console.log('相亲了')被执行),就通过resolve来告诉这个Promise:“嘿,我完成任务啦,你可以把状态更新为成功啦。”
当Promise的状态变为fulfilled后,会触发与这个Promise相关联的.then()方法中的回调函数
即 date().then,完整代码如下
function date() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲了');
resolve()
}, 2000)
})
}
function marry() {
setTimeout(() => {
console.log('你结婚了');
}, 1000)
}
date().then(() => {
marry()
})
这样就正确输出了
是不是很丝滑! 接下来如果你还想生宝宝呢,呢是不是还是得哄哄你的媳妇,给一点promise(承诺),以后宝宝归我带,毕竟十月怀胎是很辛苦的! 我们再加一个
function baby() {
console.log('出生了');
}
如果你是直接加到date().then()里显然是不可以的,先生宝宝再结婚。不能加到date().then()里。
我们需要加到marry()里,我们要让marry有许诺的能力
function date() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲了');
resolve()
}, 2000)
})
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('结婚了');
resolve()
}, 1000)
})
}
function baby() {
console.log('出生了');
}
date().then(() => {
marry().then(() => {
baby()
})
})
但是如果一旦事情嵌套的多了,这样写then不太优雅,代码会很臃肿,所以官方将其优化了一下,我先不告诉大家正确的用法,先挖个坑,这个能正确输出吗
xq()
.then(() => {
marry()
}).then(() => {
baby()
})
答案是NO,呢这是为什么呢,看起来很合理但是不行。我们来详细解释一下
- 当
date()被调用时,它返回的Promise开始执行。这个Promise内部通过setTimeout模拟一个异步操作,等待 2 秒后输出'相亲了',然后调用resolve,此时这个Promise的状态变为fulfilled(已成功)。 - 一旦
date()返回的Promise状态变为fulfilled,第一个.then中的回调函数() => { marry() }就会被执行。这里需要注意的是,虽然marry函数被调用了,但由于没有返回marry函数的返回值,这个.then实际上返回了一个默认的Promise对象,其状态是pending。 - 而
marry函数本身返回一个Promise,它内部通过setTimeout模拟一个异步操作,等待 1 秒后输出'结婚了',然后调用resolve,使自己的状态变为fulfilled。但是这个状态变化暂时没有被有效地传递给后续的.then,因为前面的.then没有正确地返回marry的Promise。 - 第一个
.then返回的默认Promise(状态为pending)等待状态变化。当marry函数返回的Promise状态变为fulfilled时,由于第一个.then没有正确地返回这个Promise,第二个.then暂时无法直接获取到这个状态变化来执行。 - 不过,在 JavaScript 的 Promise 链式调用机制下,它会继续往前查找已经确定状态(
fulfilled或rejected)的Promise。当它发现marry返回的Promise状态变为fulfilled后,第二个.then中的回调函数() => { baby() }就会被执行,从而在控制台输出'出生了'。
简单的说就是
date()返回的 Promise 在 2 秒后变为fulfilled,触发第一个.then,调用marry()但未正确返回其 Promise,导致第一个.then返回默认的pending状态的 Promise。marry自身返回的 Promise 在 1 秒后变为fulfilled,但由于第一个.then未正确返回,第二个.then暂时无法直接获取该状态变化。- 在 Promise 链式调用机制下,第二个
.then会继续往前查找已确定状态的 Promise,当发现marry的 Promise 变为fulfilled后,执行第二个.then的回调函数输出'出生了'。
当你理解了之后我们给出你正确的使用方法
xq()
.then(() => {
return marry()
}).then(() => {
baby()
})
为什么是return marry呢,这是因为在第一个.then中调用return marry()的话,可将marry函数返回的 Promise 对象传递给下一个环节,确保 Promise 链式调用能正确处理异步操作顺序。若没有此返回语句,第一个.then会默认返回状态为pending的新 Promise 对象,后续.then可能无法正确获取前面异步操作结果。
🐟传递成功结果值
resolve(x)不仅会转变Promise状态,同时resolve可以传递一个参数,这个参数会作为成功的值传递给.then方法中的回调函数。即传递x,我们改造上面的函数,来简单的运行一下
function date() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲了');
// 可以传递一个对象表示相亲的结果
resolve('和最爱的人相亲了');
}, 2000);
});
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('结婚了');
// 传递结婚的相关信息
resolve('和最爱的人结婚了');
}, 1000);
});
}
function baby() {
console.log('出生了');
}
date()
.then(resultFromDate => {
console.log('相亲结果:', resultFromDate);
return marry();
})
.then(resultFromMarry => {
console.log('结婚信息:', resultFromMarry);
baby();
});
是不是很nice, resolve传递的参数可以是任何 JavaScript 数据类型,如对象、数组、字符串、数字等。这里我们就不一一实验了,他的用途有很多!大家可以自行深入了解!
下面我们来介绍一下rejected
🐟rejected
在 Promise 中,“rejected” 状态表示 Promise 被拒绝,通常因异步操作失败或出错。
触发方式:可在 Promise 的执行函数内部通过调用reject()来触发 “rejected” 状态。例如:
function asyncOperation() {
return new Promise((resolve, reject) => {
// 模拟异步操作失败
setTimeout(() => {
reject(new Error("Async operation failed"));
}, 2000);
});
}
处理方式:使用.catch()方法来处理 “rejected” 状态。当 Promise 被拒绝时,.catch中的回调函数会被执行,接收拒绝的原因(这里是一个错误对象)并进行相应错误处理,比如在控制台打印错误消息。例如:
asyncOperation().catch(error => {
console.log(error.message);
});
🐟 Demo
我在上面相亲结婚的基础上,加上了rejected,catch处理,方便大家理解,大家可以运行一下这个小Demo,自己上手试一试
function date() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.5;
if (isSuccess) {
console.log('相亲了');
resolve();
} else {
reject(new Error('相亲失败'));
}
}, 2000);
});
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.5;
if (isSuccess) {
console.log('结婚了');
resolve();
} else {
reject(new Error('结婚失败'));
}
}, 1000);
});
}
function baby() {
console.log('出生了');
}
date()
.then(() => {
return marry();
})
.then(() => {
baby();
})
.catch(error => {
console.error('出现错误:', error.message);
});
🐟 End
希望大家能有所收获,Promise 是一种用于处理异步操作的强大工具,它允许我们以更优雅和可读的方式处理异步代码。也是面试官最爱问的,先初步搞懂它,再去理解他的所有api,这就需要大家自己去看官方文档了,知识才会进脑子!