彻底掌握 Promise
文章目录
其他相关传送门
一、Promise 出现的原因
在 Promise 出现以前,我们处理一个异步网络请求,大概是这样:
// 请求 代表 一个异步网络调用。
// 请求结果 代表网络请求的响应。
请求1(function(请求结果1){
处理请求结果1
})
看起来还不错。
但是,需求变化了,我们需要根据第一个网络请求的结果,再去执行第二个网络请求,代码大概如下:
请求1(function(请求结果1){
请求2(function(请求结果2){
处理请求结果2
})
})
看起来也不复杂。
但是。。需求是永无止境的,于是乎出现了如下的代码:
请求1(function(请求结果1){
请求2(function(请求结果2){
请求3(function(请求结果3){
请求4(function(请求结果4){
请求5(function(请求结果5){
请求6(function(请求结果6){
...
})
})
})
})
})
})
这回傻眼了。。。 臭名昭著的 回调地狱
现身了。
更糟糕的是,我们基本上还要对每次请求的结果进行一些处理,代码会更加臃肿,在一个团队中,代码 review 以及后续的维护将会是一个很痛苦的过程。
回调地狱带来的负面作用有以下几点:
- 代码臃肿
- 可读性差
- 耦合度过高,可维护性差
- 代码复用性差
- 容易滋生 bug
- 只能在回调里处理异常
出现了问题,自然就会有人去想办法。这时,就有人思考了,能不能用一种更加友好的代码组织方式,解决异步嵌套的问题。
let 请求结果1 = 请求1();
let 请求结果2 = 请求2(请求结果1);
let 请求结果3 = 请求3(请求结果2);
let 请求结果4 = 请求2(请求结果3);
let 请求结果5 = 请求3(请求结果4);
类似上面这种同步的写法。 于是 Promise
规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点。比如业界著名的 Q 和 bluebird,bluebird 甚至号称运行最快的类库。
二、什么是 Promise?
Promise 是异步编程的一种解决方案:
- 从语法上讲,Promise 是一个 ES6 的原生对象,从它可以获取异步操作的消息;
- 从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。
- Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
Promise 有三种状态:
pending
(等待态)fulfiled
(成功态)rejected
(失败态)
一旦 Promise
被 resolve
或 reject
,不能再迁移至其他任何状态(即状态 immutable
)。
创造 promise
实例后,它会立即执行。
基本过程:
- 初始化 Promise 状态(pending);
- 立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理;
- 执行 then(…) 注册回调处理数组(then 方法可被同一个 promise 调用多次);
- Promise里的关键是要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行;
promise是用来解决两个问题的:
- 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
- promise可以支持多个并发的请求,获取并发请求中的数据
- 这个promise可以解决异步的问题,本身不能说promise是异步的
三、Promise 的优缺点
优点:
- 有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数(回调地狱)。
- 此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
缺点:
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
四、Promise 的用法
1. Promise是一个构造函数
Promise的构造函数接收一个参数:函数,并且这个函数需要传入两个参数:
- resolve :异步操作执行成功后的回调函数
- reject:异步操作执行失败后的回调函数
let p = new Promise((resolve, reject) => {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
if (num <= 5) {
resolve(num);
} else {
reject('数字太大了');
}
})
2. then 与 reject 的用法
then
中传了两个参数,then
方法可以接受两个参数,第一个对应 resolve
的回调,第二个对应 reject
的回调。
所以我们能够分别拿到他们传过来的数据。
let p = new Promise((resolve, reject) => {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
if (num <= 5) {
resolve(num);
} else {
reject('数字太大了');
}
})
p.then((data) => {
console.log('resolve:' + data); // 处理resolve回调
}, (err) => {
console.log('reject:' + err); // 处理reject回调
})
多次运行这段代码,你会随机得到下面两种结果:
resolve:5
、 reject:数字太大了
3. then的链式调用
所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:
request('test1.html', data)
.then(function (data1) {
console.log('第一次请求成功, 这是返回的数据:', data1);
return request('test2.html', data1);
})
.then(function (data2) {
console.log('第二次请求成功, 这是返回的数据:', data2);
return request('test3.html', data2);
})
.then(function (data3) {
console.log('第三次请求成功, 这是返回的数据:', data3);
})
.catch(function (error) {
console.log('sorry, 请求失败了, 这是失败信息:', error);
});
Promise对象的then方法返回一个新的Promise对象,因此可以通过链式调用then方法。
then方法接收两个函数作为参数,第一个参数是Promise执行成功时的回调,第二个参数是Promise执行失败时的回调。
两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。
这两个参数的返回值可以是以下三种情况中的一种:
- return 一个同步的值 ,或者 undefined(当没有返回一个有效值时,默认返回undefined),then方法将返回一个resolved状态的Promise对象,Promise对象的值就是这个返回值。
- return 另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回。
- throw 一个同步异常,then方法将返回一个rejected状态的Promise, 值是该异常。
var p = new Promise(function (resolve, reject) {
resolve(1);
});
p.then(function (value) {
console.log(value); // 1
return value * 2; // return一个同步的值(resolved状态)
}).then(function (value) {
console.log(value); // 2
}).then(function (value) {
console.log(value); // 上面的then没有return,因此默认是return undefined
return Promise.resolve(3);
}).then(function (value) {
console.log(value); // 接收到resolve(3)
return Promise.reject(4);
}).then(function (value) {
console.log('resolve: ' + value);
}, function (err) {
console.log('reject: ' + err); // 接收到reject(4)
})
控制台输出:
1
2
undefined
3
"reject: 4"
4. catch的用法
我们知道 Promise
对象除了 then
方法,还有一个 catch
方法,它是做什么用的呢?
其实它和 then
的第二个参数一样,用来指定 reject
的回调。 用法是这样:
let p = new Promise((resolve, reject) => {
var num = Math.ceil(Math.random() * 10); //生成1-10的随机数
if (num <= 5) {
resolve(num);
} else {
reject('数字太大了');
}
})
p.then((data) => { console.log('resolve:' + data); })
.catch((err) => { console.log('reject:' + err); })
效果和写在then的第二个参数里面一样。
不过它还有另外一个作用:在执行 resolve
的回调(也就是上面 then
中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死 js,而是会进到这个 catch
方法中。
catch 既能处理 reject 回调,也能捕捉错误
请看下面的代码:
p.then((data) => {
console.log('resolved',data);
console.log(somedata); //此处的somedata未定义
})
.catch((err) => {
console.log('rejected',err);
});
在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。
如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。
但是在这里,会得到这样的结果:
也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。
即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能
5. all的用法
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
多个 Promise 任务同时执行。 如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个Promise 任务 rejected,则只返回 rejected 任务的结果。
let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
// 三个都成功,则成功
}, function(){
// 只要有失败,则失败
})
三个都成功,则成功
只要有失败,则失败
let p1 = new Promise((resolve, reject) => {
resolve('成功了');
})
let p2 = new Promise((resolve, reject) => {
resolve('success');
})
let p3 = Promse.reject('失败');
Promise.all([p1, p2]).then((result) => {
console.log(result) //['成功了', 'success']
}).catch((error) => {
console.log(error)
})
Promise.all([p1,p3,p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 失败了,打出 '失败'
})
Promse.all
在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个 ajax
的数据回来以后才正常显示,在此之前只显示 loading
图标。
showLoading(); // 显示loading
let p1 = new Promise((resolve, reject) => {
resolve('请求1成功');
})
let p2 = new Promise((resolve, reject) => {
resolve('请求2成功');
})
Promise.all([p1, p2]).then(() => {
hideLoading(); // 消失loading
})
需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。
6. race的用法
顾名思义,Promse.race
就是赛跑的意思,意思就是说,
Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打开的是 'failed'
})
race的使用场景:比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:
//请求某个图片资源
function requestImg(){
var p = new Promise((resolve, reject) => {
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = '图片的路径';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()]).then((data) =>{
console.log(data);
}).catch((err) => {
console.log(err);
});
requestImg
函数会异步请求一张图片,我把地址写为"图片的路径",所以肯定是无法成功请求到的。timeout
函数是一个延时5秒的异步操作。我们把这两个返回Promise
对象的函数放进race
,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then
方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout
就跑赢了,则进入catch
,报出“图片请求超时”的信息。
7. Promise.resolve()
var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
resolve(1);
});
var p4 = new Promise(function(resolve, reject){
resolve(p1);
});
console.log(p1 === p2);
console.log(p1 === p3);
console.log(p1 === p4);
console.log(p3 === p4);
p4.then(function(value){
console.log('p4=' + value);
});
p2.then(function(value){
console.log('p2=' + value);
})
p1.then(function(value){
console.log('p1=' + value);
})
控制台输出:
true
false
false
false
p2=1
p1=1
p4=1
Promise.resolve(...)
可以接收一个 值 或者是一个 Promise对象
作为参数。
当参数是普通值时,它返回一个resolved状态的Promise对象,对象的值就是这个参数;
当参数是一个Promise对象时,它直接返回这个Promise参数。因此,p1 === p2。
但通过new的方式创建的Promise对象都是一个新的对象,因此后面的三个比较结果都是false。
另外,为什么p4的then最先调用,但在控制台上是最后输出结果的呢?
因为p4的resolve中接收的参数是一个Promise对象p1,resolve会对p1”拆箱“,获取p1的状态和值,但这个过程是异步的
.
无论是 then
还是 catch
,只要不报错,都返回的是 resolved
状态的 promise
。
报错则返回的是 rejected
状态的 promise
。
Promise.resolve().then(() => {
console.log(1); // 不报错,返回的是resolved状态的promise,触发then回调
}).catch(() => {
console.log(2); // 不触发
}).then(() => {
console.log(3); // 触发
})
// 1
// 3
Promise.resolve().then(() => {
console.log(1);
throw new Error('error'); // 报错,返回的是rejected状态的promise,触发catch回调
}).catch(() => {
console.log(2); // 不报错,返回的是resolved状态的promise,触发then回调
}).then(() => {
console.log(3); // 触发
})
// 1
// 2
// 3
本文作者:猫老板的豆
本文链接:https://www.cnblogs.com/bingcola/p/16499142.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步