promise详解精华
# 为什么需要 Promise?
promise是ES6中用来处理异步操作的。如果不使用promise,就需要使用回调函数来处理异步操作后的结果,此时如果有多个有顺序的异步操作,会造成回调的嵌套,引发回调地狱。
# 回调的定义
把函数 A 传给另一个函数 B 调用,那么函数 A 就是回调函数。
例如在浏览器中发送 ajax 请求,就是常见的个异步场景,发送请求后,需要等待一段时间,等服务端响应之后我们才能拿到结果。如果我们希望在异步结束之后执行某个操作,就只能通过回调函数这样的方式进行操作。
var dynamicFunc = function (callback) {
setTimeout(function () {
callback();
}, 1000);
};
dynamicFunc(function () {
console.log('test');
});
例如上面这个例子,dynamicFunc 就是个异步函数,执行 setTimeout 会在 1s 之后调用 callback 函数。按照上面的调用方式,最终 1s 之后,会打印 test 这个结果。
为了能使回调函数以更优雅的方式进行调用,在 ES6 语法中,新增了函数个名为 Promise 的新规范。
# 回调的缺点
回调的写法比较直观,不需要 return,层层嵌套即可。但也存在两个问题:
- 如果嵌套过深,则会出现回调地狱的问题。
- 不同的函数,回调的参数,在写法上可能不一致,导致不规范、且需要单独记忆。
我们来具体看看这两个问题。
1、回调地狱的问题:
如果多个异步函数存在依赖关系(比如,需要等第一个异步函数执行完成后,才能执行第二个异步函数;等第二个异步函数执行完毕后,才能执行第三个异步函数),就需要多个异步函数进⾏层层嵌套,⾮常不利于后续的维护,而且会导致回调地狱的问题。
关于回调地狱,我们来举一个形象的例子:
假设买菜、做饭、洗碗、倒厨余垃圾都是异步的。
但真实的场景中,实际的操作流程是:买菜成功之后,才能开始做饭。做饭成功后,才能开始洗碗。洗碗完成后, 再倒厨余垃圾。这里的一系列动作就涉及到了多层嵌套调用,也就是回调地狱。
关于回调地狱,我们来看看几段代码举例。
1.1、定时器的代码举例:(回调地狱)
setTimeout(function () {
console.log('qiangu1');
setTimeout(function () {
console.log('qiangu2');
setTimeout(function () {
console.log('qiangu3');
}, 3000);
}, 2000);
}, 1000);
1.2、Node.js 读取文件的代码举例:(回调地狱)
fs.readFile(A, 'utf-8', function (err, data) {
fs.readFile(B, 'utf-8', function (err, data) {
fs.readFile(C, 'utf-8', function (err, data) {
fs.readFile(D, 'utf-8', function (err, data) {
console.log('qianguyihao:' + data);
});
});
});
});
上面代码的逻辑为:先读取 A 文本内容,再根据 A 文本内容读取 B,然后再根据 B 的内容读取 C。为了实现这个业务逻辑,上面的代码就很容易形成回调地狱。
1.3、ajax 请求的代码举例:(回调地狱)
// 伪代码
ajax('a.json', (res1) => {
console.log(res1);
ajax('b.json', (res2) => {
console.log(res2);
ajax('c.json', (res3) => {
console.log(res3);
});
});
});
2、回调的写法不一致问题:
// Node.js 读取文件时,成功回调和失败回调,是通过 error参数来区分
readFile('d:\\readme.text', function (err, data) {
if (error) {
console.log('文件读取失败');
} else {
console.log('文件读取成功');
}
});
// jQuery的 ajax 写法中,成功回调和失败回调,是通过两个回调函数来区分
$.ajax({
url: '/ajax.json',
success: function (response) {
console.log('文件读取成功');
},
error: function (err) {
console.log('文件读取失败');
},
});
我们可以看到,上面的代码中,成功回调和失败回调,写法不统一,需要单独记忆,容易出错。
小结:
在 ES5 中,当进行多层嵌套回调时,会导致代码层次过多,很难进行后续维护和二次开发;而且会导致回调地狱的问题。ES6 中的 Promise 就可以解决这些问题。
当然, Promise 的强大功能,不止于此。我们来一探究竟。
# Promise 的介绍和优点
ES6 中的 Promise 是异步编程的一种方案。从语法上讲,Promise 是一个对象,它可以获取异步操作的消息。
Promise 对象, 可以用同步的表现形式来书写异步代码(也就是说,代码看起来是同步的,但本质上的运行过程是异步的)。使用 Promise 主要有以下好处:
- 1、可以很好地解决回调地狱的问题(避免了层层嵌套的回调函数)。
- 2、语法简洁、可读性强,便于后期维护。
- 3、Promise 对象提供了简洁的 API,使得管理异步操作更加容易。比如多任务等待合并。
Promise 的伪代码结构,大概是这样的:
// 伪代码1
myPromise()
.then(
function () {},
function () {}
)
.then(
function () {},
function () {}
)
.then(
function () {},
function () {}
);
// 伪代码2
是时候展现真正的厨艺了().然后(买菜).然后(做饭).然后(洗碗);
上面的伪代码可以看出,即便在业务逻辑上是层层嵌套,但是代码写法上,却十分优雅,也没有过多的嵌套。
# Promise 对象的用法和状态
# 使用 Promise 的基本步骤
(1)通过 new Promise()
构造出一个 Promise 实例。Promise 的构造函数中传入一个参数,这个参数是一个函数,该函数用于处理异步任务。
(2)函数中传入两个参数:resolve 和 reject,分别表示异步执行成功后的回调函数和异步执行失败后的回调函数。代表着我们需要改变当前实例的状态到已完成或是已拒绝。
(3)通过 promise.then() 和 promise.catch() 处理返回结果(这里的 promise
指的是 Promise 实例)。
看到这里,你估计还是不知道 Promise 怎么使用。我们不妨来看一下 Promise 有哪些状态,便一目了然。要知道,Promise 的精髓在于对异步操作的状态管理。
# promise 对象的 3 个状态
初始化(等待中):pending
成功:fulfilled
失败:rejected
步骤 1:
当 new Promise()执行之后,promise 对象的状态会被初始化为pending
,这个状态是初始化状态。new Promise()
这行代码,括号里的内容是同步执行的。括号里可以再定义一个 异步任务的 function,function 有两个参数:resolve 和 reject。如下:
如果请求成功了,则执行 resolve(),此时,promise 的状态会被自动修改为 fulfilled。
如果请求失败了,则执行 reject(),此时,promise 的状态会被自动修改为 rejected
(2)promise.then()方法:只有 promise 的状态被改变之后,才会走到 then 或者 catch。也就是说,在 new Promise()的时候,如果没有写 resolve(),则 promise.then() 不执行;如果没有写 reject(),则 promise.catch() 不执行。
then()
括号里面有两个参数,分别代表两个函数 function1 和 function2:
如果 promise 的状态为 fulfilled(意思是:如果请求成功),则执行 function1 里的内容
如果 promise 的状态为 rejected(意思是,如果请求失败),则执行 function2 里的内容
另外,resolve()和 reject()这两个方法,是可以给 promise.then()传递参数的。
关于 promise 的状态改变,以及如何处理状态改变,伪代码及注释如下:
// 创建 promise 实例
let promise = new Promise((resolve, reject) => {
//进来之后,状态为pending
console.log('同步代码'); //这行代码是同步的
//开始执行异步操作(这里开始,写异步的代码,比如ajax请求 or 开启定时器)
if (异步的ajax请求成功) {
console.log('333');
resolve('请求成功,并传参'); //如果请求成功了,请写resolve(),此时,promise的状态会被自动修改为fulfilled(成功状态)
} else {
reject('请求失败,并传参'); //如果请求失败了,请写reject(),此时,promise的状态会被自动修改为rejected(失败状态)
}
});
console.log('222');
//调用promise的then():开始处理成功和失败
promise.then(
(successMsg) => {
// 处理 promise 的成功状态:如果promise的状态为fulfilled,则执行这里的代码
console.log(successMsg, '成功了'); // 这里的 successMsg 是前面的 resolve('请求成功,并传参') 传过来的参数
},
(errorMsg) => {
//处理 promise 的失败状态:如果promise的状态为rejected,则执行这里的代码
console.log(errorMsg, '失败了'); // 这里的 errorMsg 是前面的 reject('请求失败,并传参') 传过来的参数
}
);
上面的注释要多看几遍。
# 几点补充
# new Promise() 是同步代码
new Promise()
这行代码本身是同步的。promise 如果没有使用 resolve 或 reject 更改状态时,状态为 pending。
举例 1:
const promiseA = new Promise((resolve, reject) => {});
console.log(promiseA); // 此时 promise 的状态为 pending(准备阶段)
上面的代码中,我既没有写 reslove(),也没有写 reject()。也就是说,这个 promise 一直处于准备阶段。
当完成异步任务之后,状态分为成功或失败,此时我们就可以用 reslove() 和 reject() 来修改 promise 的状态。
举例 2:
new Promise((resolve, reject) => {
console.log('promise1'); // 这行代码是同步代码,会立即执行
}).then((res) => {
console.log('promise then:' + res); // 这行代码不会执行,因为前面没有写 resolve(),所以走不到 .then
});
打印结果:
promise1
上方代码,仔细看注释:如果前面没有写 resolve()
,那么后面的 .then
是不会执行的。
举例 3:
new Promise((resolve, reject) => {
resolve();
console.log('promise1'); // 代码1:同步任务,会立即执行
}).then(res => {
console.log('promise then'); // 代码2:异步任务中的微任务
});
console.log('千古壹号'); // 代码3:同步任务
打印结果:
promise1
千古壹号
promise then
代码解释:代码 1 是同步代码,所以最先执行。代码 2 是微任务里面的代码,所以要先等同步任务(代码 3)先执行完。当写完resolve();
之后,就会立刻把 .then()
里面的代码加入到微任务队列当中。
补充知识:异步任务分为“宏任务”、“微任务”两种。我们到后续的章节中再详细讲。
# Promise 的状态一旦改变,就不能再变
代码举例:
const p = new Promise((resolve, reject) => {
resolve(1); // 代码执行到这里时, promise状态是 fulfilled
reject(2); // 尝试修改状态为 rejected,是不行的。因为状态执行到上一行时,已经被改变了。
});
p.then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
});
上方代码的打印结果是 1,而不是 2,详见注释。
# Promise 的状态改变,是不可逆的
# 小结
1、promise 有三种状态:等待中、成功、失败。等待中状态可以更改为成功或失败,已经更改过状态后⽆法继续更改(例如从失败改为成功)。
2、promise 实例中需要传⼊⼀个函数,这个函数接收两个参数,执⾏第⼀个参数之后就会改变当前 promise 为「成功」状态,执⾏第⼆个参数之后就会变为「失败」状态。
3、通过 .then ⽅法,即可在上⼀个 promise 达到成功时继续执⾏下⼀个函数或 promise。同时通过 resolve 或 reject 时传⼊参数,即可给下⼀个函数或 promise 传⼊初始值。
4、失败的 promise,后续可以通过 promise 自带的 .catch ⽅法或是 .then ⽅法的第⼆个参数进⾏捕获。
# Promise 规范
Promise 是⼀个拥有 then ⽅法的对象或函数。任何符合 promise 规范的对象或函数都可以成为 Promise。
关于 promise 规范的详细解读,可以看下面这个链接:
- Promises/A+ 规范:https://promisesaplus.com/ (opens new window)
了解这些常见概念之后,接下来,我们来具体看看 promise 的代码是怎么写的。
# Promise 封装定时器
# 传统写法
写法 1:
// 定义一个异步的延迟函数:异步函数结束1秒之后,再执行cb回调函数
function fun1(cb) {
setTimeout(function () {
console.log('即将执行cb回调函数');
cb();
}, 1000);
}
// 先执行异步函数 fun1,再执行回调函数 myCallback
fun1(myCallback);
// 定义回调函数
function myCallback() {
console.log('我是延迟执行的cb回调函数');
}
写法 2:(精简版,更常见)
// 定义一个异步的延迟函数:异步函数结束1秒之后,再执行cb回调函数
function fun1(cb) {
setTimeout(cb, 1000);
}
// 先执行异步函数fun1,再执行回调函数
fun1(function () {
console.log('我是延迟执行的cb回调函数');
});
上⾯的例⼦就是最传统的写法,在异步结束后通过传入回调函数的方式执⾏函数。
学习 Promise 之后,我们可以将这个异步函数封装为 Promise,如下。
# Promise 写法
function myPromise() {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}
/* 【重要】上面的 myPromise 也可以写成:
function myPromise() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
}
*/
// 先执行异步函数 myPromise,再执行回调函数
myPromise().then(() => {
console.log('我是延迟执行的回调函数');
});
# Promise 封装 Ajax 请求
# 传统写法
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
// 这里的 && 符号,意思是:如果传了 fail 参数,就调用后面的 fail();如果没传 fail 参数,就不调用后面的内容。因为 fail 参数不一定会传。
fail && fail(new Error('接口请求失败'));
}
};
}
// 执行 ajax 请求
ajax(
'/a.json',
(res) => {
console.log('qianguyihao 第一个接口请求成功:' + JSON.stringify(res));
},
(err) => {
console.log('qianguyihao 请求失败:' + JSON.stringify(err));
}
);
上面的传统写法里,定义和执行 ajax 时需要传⼊ success 和 fail 这两个回调函数,进而执行回调函数。
注意看注释,callback && callback()
这种格式的写法,很常见。
# Promise 写法
有了 Promise 之后,我们不需要传入回调函数,而是:
先将 promise 实例化;
然后在原来执行回调函数的地方,改为执行对应的改变 promise 状态的函数;
并通过 then ... catch 或者 then ...then 等写法,实现链式调用,提高代码可读性。
和传统写法相比,promise 在写法上的大致区别是:定义异步函数的时候,将 callback 改为 resolve 和 reject,待状态改变之后,我们在外面控制具体执行哪些函数。
写法 1:
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
// 这里的 && 符号,意思是:如果传了 fail 参数,就调用后面的 fail();如果没传 fail 参数,就不调用后面的内容。因为 fail 参数不一定会传。
fail && fail(new Error('接口请求失败'));
}
};
}
// 第一步:model层的接口封装
function promiseA() {
return new Promise((resolve, reject) => {
ajax('xxx_a.json', (res) => {
// 这里的 res 是接口的返回结果。返回码 retCode 是动态数据。
if (res.retCode == 0) {
// 接口请求成功时调用
resolve('request success' + res);
} else {
// 接口请求失败时调用
reject({ retCode: -1, msg: 'network error' });
}
});
});
}
// 第二步:业务层的接口调用。这里的 data 就是 从 resolve 和 reject 传过来的,也就是从接口拿到的数据
promiseA()
.then((res) => {
// 从 resolve 获取正常结果:接口请求成功后,打印接口的返回结果
console.log(res);
})
.catch((err) => {
// 从 reject 获取异常结果
console.log(err);
});
上方代码中,当从接口返回的数据data.retCode
的值(接口返回码)不同时,可能会走 resolve,也可能会走 reject,这个由你自己的业务决定。
接口返回的数据,一般是{ retCode: 0, msg: 'qianguyihao' }
这种 json 格式, retCode 为 0 代表请求接口成功,所以前端对应会写if (res.retCode == 0)
这样的逻辑。
另外,上面的写法中,是将 promise 实例定义成了一个函数 promiseA
。我们也可以将 promise 实例定义成一个变量 promiseB
,达到的效果和上面的代码是一模一样的。写法如下:(写法上略有区别)
写法 2:
// 第一步:model层的接口封装
const promiseB = new Promise((resolve, reject) => {
ajax('xxx_a.json', (res) => {
// 这里的 res 是接口的返回结果。返回码 retCode 是动态数据。
if (res.retCode == 0) {
// 接口请求成功时调用
resolve('request success' + res);
} else {
// 接口请求失败时调用
reject({ retCode: -1, msg: 'network error' });
}
});
});
// 第二步:业务层的接口调用。这里的 data 就是 从 resolve 和 reject 传过来的,也就是从接口拿到的数据
promiseB
.then((res) => {
// 从 resolve 获取正常结果
console.log(res);
})
.catch((err) => {
// 从 reject 获取异常结果
console.log(err);
});
注意,如果你用的是写法 1(将 promise 实例定义为函数),则调用 promise 的时候是promiseA().then()
,如果你用的是写法 2(将 promise 实例定位为函数),则调用的时候用的是promiseB.then()
。写法 1 多了个括号,不要搞混了。
# 处理 reject 失败状态的两种写法
我们有两种写法可以捕获并处理 reject 异常状态:
写法 1:通过 catch 方法捕获 状态变为已 reject 时的 promise
写法 2:then 可以传两个参数,第⼀个参数为 resolve 后执⾏,第⼆个参数为 reject 后执⾏。
# 代码格式
这两种写法的代码格式如下:
// 第一步:model层的接口封装
function promiseA() {
return new Promise((resolve, reject) => {
// 这里做异步任务(比如 ajax 请求接口,或者定时器)
...
...
});
}
const onResolve = function (res) {
console.log(res);
};
const onReject = function (err) {
console.log(err);
};
// 写法1:通过 catch 方法捕获 状态变为已拒绝时的 promise
promiseA().then(onResolve).catch(onReject);
// 写法2:then 可以传两个参数,第⼀个参数为 resolve 后执⾏,第⼆个参数为 reject 后执⾏
promiseA().then(onResolve, onReject);
// 【错误写法】写法3:通过 try catch 捕获 状态变为已拒绝时的 promise
// 这种写法是错误的,因为 try catch只能捕获同步代码里的异常,而 promise.reject() 是异步代码。
try {
promiseA().then(onResolve);
} catch (e) {
// 语法上,catch必须要传入一个参数,否则报错
onReject(e);
}
如注释所述:前面的段落里,我们捕获 reject 异常用的就是写法 1。如果你写法 2 也是可以的。
需要注意的是,上面的写法 3 是错误的。运行之后,控制台会报如下错误:
try-catch 主要用于捕获异常,注意,这里的异常是指同步函数的异常。如果 try 里面的异步方法出现了异常,此时 catch 是无法捕获到异常的。
原因是:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言(比如 promise)promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。
(2)写法 1 中,promiseA().then().catch()
和promiseA().catch().then()
区别在于:前者可以捕获到 then
里面的异常,后者不可以。
# 代码举例
这两种写法的代码举例如下:
function promiseA() {
return new Promise((resolve, reject) => {
// 这里做异步任务(比如 ajax 请求接口,或者定时器)
...
...
});
}
// 写法1
promiseB()
.then((res) => {
// 从 resolve 获取正常结果
console.log('接口请求成功时,走这里');
console.log(res);
})
.catch((err) => {
// 从 reject 获取异常结果
console.log('接口请求失败时,走这里');
console.log(err);
})
.finally(() => {
console.log('无论接口请求成功与否,都会走这里');
});
// 写法 2:(和写法 1 等价)
promiseB()
.then(
(res) => {
// 从 resolve 获取正常结果
console.log('接口请求成功时,走这里');
console.log(res);
},
(err) => {
// 从 reject 获取异常结果
console.log('接口请求失败时,走这里');
console.log(err);
}
)
.finally(() => {
console.log('无论接口请求成功与否,都会走这里');
});
代码解释:写法 1 和写法 2 的作用是完全等价的。只不过,写法 2 是把 catch 里面的代码作为 then 里面的第二个参数而已。
# 参考链接
# Promise 的链式调用:处理多次 Ajax 请求【重要】
实际开发中,我们经常需要同时请求多个接口。比如说:在请求完接口1
的数据data1
之后,需要根据data1
的数据,继续请求接口 2,获取data2
;然后根据data2
的数据,继续请求接口 3。
换而言之,现在有三个网络请求,请求 2 必须依赖请求 1 的结果,请求 3 必须依赖请求 2 的结果,如果按照往常的写法,会有三层回调,会陷入“回调地狱”。
这种场景其实就是接口的多层嵌套调用。有了 Promise 之后,我们可以把多层嵌套调用按照线性的方式进行书写,非常优雅。也就是说:Promise 可以把原本的多层嵌套写法改进为链式写法。
# ES5 中的传统写法
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}
// 执行 ajax 请求
ajax(
'/a.json',
(res) => {
console.log('qianguyihao 第一个接口请求成功:' + JSON.stringify(res));
// ajax嵌套调用
ajax('b.json', (res) => {
console.log('qianguyihao 第二个接口请求成功:' + JSON.stringify(res));
// ajax嵌套调用
ajax('c.json', (res) => {
console.log('qianguyihao 第三个接口请求成功:' + JSON.stringify(res));
});
});
},
(err) => {
console.log('qianguyihao 请求失败:' + JSON.stringify(err));
}
);
上面的代码层层嵌套,可读性很差,而且出现了我们常说的回调地狱问题。
# Promise 链式调用(初步写法,方便理解)
如果我们不对 Promise 的链式调用进行封装,那么,它的简单写法是下面这样的:
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}
new Promise((resolve, reject) => {
ajax('a.json', (res) => {
console.log(res);
resolve();
});
})
.then((res) => {
console.log('a成功');
return new Promise((resolve, reject) => {
ajax('b.json', (res) => {
console.log(res);
resolve();
});
});
})
.then((res) => {
console.log('b成功');
return new Promise((resolve, reject) => {
ajax('c.json', (res) => {
console.log(res);
resolve();
});
});
})
.then((res) => {
cnosole.log('c成功');
});
上面代码中,then 是可以链式调用的,一旦 return 一个新的 promise 实例之后,后面的 then 就可以拿到前面 resolve 出来的数据。这种扁平化的写法,更方便维护;并且可以更好的管理请求成功和失败的状态。
但是,你可能会奇怪,上面的代码,怎么这么多?而且有不少重复。因为这里只是采用了一种笨拙的方式来写,为的是方便大家理解 promise 的执行过程。我们其实可以对 promise 的链式调用进行封装。
怎么个封装法呢?上面的代码中,每次在 return 一个 promise 的时候,只是 url 地址不一样,其他的代码是一样的。所以我们可以把重复的代码封装成函数。写法如下。
# Promise 链式调用(封装一个接口)
针对同一个接口的多次嵌套调用,采用 promise 封装后的写法如下:
// 定义 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}
// 第一步:model层,接口封装
function getPromise(url) {
return new Promise((resolve, reject) => {
ajax(url, (res) => {
// 这里的 res 是接口的返回结果。返回码 retCode 是动态数据。
if (res.retCode == 0) {
// 接口请求成功时调用
resolve('request success' + res);
} else {
// 接口请求异常时调用
reject({ retCode: -1, msg: 'network error' });
}
});
});
}
// 第二步:业务层的接口调用。这里的 data 就是 从 resolve 和 reject 传过来的,也就是从接口拿到的数据
getPromise('a.json')
.then((res) => {
// a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
console.log(res);
return getPromise('b.json'); // 继续请求 b
})
.then((res) => {
// b 请求成功
console.log(res);
return getPromise('c.json'); // 继续请求 c
})
.then((res) => {
// c 请求成功
console.log(res);
})
.catch((e) => {
// 从 reject中获取异常结果
console.log(e);
});
怎么样?上面代码是不是非常简洁?而且可读性很强。
代码写到这里,我们还可以再继续优化一下。细心的你可以发现,我们在做三次嵌套请求的时候,针对 resolve 和 reject 的处理时机是一样的。如果你的业务是针对同一个接口连续做了三次调用,只是请求传参不同,那么,按上面这样写是没有问题的。
但是,真正在实战中,我们往往需要嵌套请求多个不同的接口,要处理的 resolve 和 reject 的时机和逻辑往往是不同的,所以需要分开封装不同的 Promise 实例,这在实战开发中更为常见。代码应该是像下面这样写。
# Promise 链式调用(封装多个接口)
针对多个不同接口的嵌套调用,采用 promise 封装后的写法如下:
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}
// Promise 封装接口1
function request1() {
return new Promise((resolve, reject) => {
ajax('https://www.baidu.com', (res) => {
if (res.retCode == 201) {
// 接口请求成功时调用:这里的 res 是接口1的返回结果
resolve('request1 success' + res);
} else {
// 接口请求异常时调用异常
reject('接口1请求失败');
}
});
});
}
// Promise 封装接口2
function request2() {
return new Promise((resolve, reject) => {
ajax('https://www.jd.com', (res) => {
if (res.retCode == 202) {
// 这里的 res 是接口2的返回结果
resolve('request2 success' + res);
} else {
reject('接口2请求失败');
}
});
});
}
// Promise 封装接口3
function request3() {
return new Promise((resolve, reject) => {
ajax('https://www.taobao.com', (res) => {
if (res.retCode == 203) {
// 这里的 res 是接口3的返回结果
resolve('request3 success' + res);
} else {
reject('接口3请求失败');
}
});
});
}
// 先发起request1,等resolve后再发起request2;紧接着,等 request2有了 resolve之后,再发起 request3
request1()
.then((res1) => {
// 接口1请求成功
console.log(res1);
return request2();
})
.then((res2) => {
// 接口2请求成功
console.log(res2);
return request3();
})
.then((res3) => {
// 接口3请求成功
console.log(res3);
})
.catch((err) => {
// 从 reject中获取异常结果
console.log(err);
});
这段代码很经典,你一定要多看几遍,多默写几遍。倒背如流也不过分。
# Promise 链式调用:封装 Node.js 的回调方法
# 传统写法
fs.readFile(A, 'utf-8', function (err, data) {
fs.readFile(B, 'utf-8', function (err, data) {
fs.readFile(C, 'utf-8', function (err, data) {
fs.readFile(D, 'utf-8', function (err, data) {
console.log('qianguyihao:' + data);
});
});
});
});
上方代码多层嵌套,存在回调地狱的问题。
# Promise 写法
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
read(A)
.then((data) => {
return read(B);
})
.then((data) => {
return read(C);
})
.then((data) => {
return read(D);
})
.then((data) => {
console.log('qianguyihao:' + data);
})
.catch((err) => {
console.log(err);
});
这一段代码可以看出,Promise 很好的处理了回调地狱的问题。下一篇文章,我们会更详细的介绍 Promise 的链式调用。
# 链式调用,如何处理 reject 失败状态
# 例 1:不处理 reject
getPromise('a.json')
.then(
(res) => {
console.log(res);
return getPromise('b.json'); // 继续请求 b
},
(err) => {
// a 请求失败
console.log('a: err');
}
)
.then((res) => {
// b 请求成功
console.log(res);
return getPromise('c.json'); // 继续请求 c
})
.then((res) => {
// c 请求成功
console.log('c:success');
});
上面的代码中,假设 a 请求失败,那么,后面的代码会怎么走呢?
打印结果:
a: err
undefined
c:success
我们可以看到,虽然 a 请求失败,但后续的请求依然会继续执行。
为何打印结果的第二行是 undefined?这是因为,当 a 请求走到 reject 之后,我们并没有做任何处理。这就导致,代码走到第二个 then
的时候,其实是在执行一个空的 promise。
# 例 2:单独处理 reject
getPromise('a.json')
.then(
(res) => {
console.log(res);
return getPromise('b.json'); // 继续请求 b
},
(err) => {
// a 请求失败
console.log('a: err');
// 【重要】即使 a 请求失败,也依然继续执行 b请求
return getPromise('b.json');
}
)
.then((res) => {
// b 请求成功
console.log(res);
return getPromise('c.json'); // 继续请求 c
})
.then((res) => {
// c 请求成功
console.log('c:success');
});
跟例 1 相比,例 2 在 reject 中增加了一行return getPromise('b.json')
,意味着,即使 a 请求失败,也要继续执行 b。
这段代码,我们是单独处理了 a 请求失败的情况。
# 统一处理 reject
针对 a、b、c 这三个请求,不管哪个请求失败,我都希望做统一处理。这种代码要怎么写呢?我们可以在最后面写一个 catch。
代码举例如下:
getPromise('a.json')
.then((res) => {
console.log(res);
return getPromise('b.json'); // 继续请求 b
})
.then((res) => {
// b 请求成功
console.log(res);
return getPromise('c.json'); // 继续请求 c
})
.then((res) => {
// c 请求成功
console.log('c:success');
})
.catch((err) => {
// 统一处理请求失败
console.log(err);
});
上面的代码中,由于是统一处理多个请求的异常,所以只要有一个请求失败了,就会马上走到 catch,剩下的请求就不会继续执行了。比如说:
a 请求失败:然后会走到 catch,不执行 b 和 c
a 请求成功,b 请求失败:然后会走到 catch,不执行 c。
# return 的返回值
return 后面的返回值,有两种情况:
情况 1:返回 Promise 实例对象。返回的该实例对象会调用下一个 then。
情况 2:返回普通值。返回的普通值会直接传递给下一个 then,通过 then 参数中函数的参数接收该值。
我们针对上面这两种情况,详细解释一下。
# 情况 1:返回 Promise 实例对象
举例如下:
getPromise('a.json')
.then((res) => {
// a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
console.log(res);
// 这里的 return,返回的是 Promise 实例对象
return new Promise((resolve, reject) => {
resolve('qianguyihao');
});
})
.then((res) => {
console.log(res);
});
# 情况 2:返回 普通值
getPromise('a.json')
.then((res) => {
// a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
console.log(res);
// 返回普通值
return 'qianguyihao';
})
/*
既然上方代码并没有返回 promise,那么,这里的 then 是谁来调用呢?
答案是:这里会产生一个新的 默认的 promise实例,来调用这里的then,确保可以继续进行链式操作。
*/
.then((res2) => {
// 这里的 res2 接收的是 普通值 'qianguyihao'
console.log(res2);
});
# Promise 的常用 API 分类
# Promise 的实例方法
实例方法:我们需要先 new 一个 promise 实例对象,然后通过 promise 对象去调用 then
、catch
、finally
方法。这几个方法就是 Promise 的实例方法。
Promise 的自带 API 提供了如下实例方法:
promise.then():获取异步任务的正常结果。
promise.catch():获取异步任务的异常结果。
promise.finaly():异步任务无论成功与否,都会执行。
# Promise 的静态方法
前面的几篇文章,讲的都是 Promise 的实例方法;今天这篇文章,我们来详细讲一下 Promise 的静态方法。
静态方法:可以直接通过大写的Promise.xxx
调用的方法。这里的xxx
就称之为静态方法。
Promise 的自带 API 提供了如下静态方法:
Promise.resolve()
Promise.reject()
Promsie.all()
:并发处理多个异步任务,所有任务都执行成功,才算成功(走到 resolve);只要有一个失败,就会马上走到 reject,整体都算失败。Promise.race()
:并发处理多个异步任务,返回的是第一个执行完成的 promise,且状态和第一个完成的任务状态保持一致。Promise.allSettled()
:并发处理多个异步任务,返回所有任务的执行结果(包括成功、失败)。当你有多个彼此不依赖的异步任务执行完成时,或者你想知道每个 promise 的结果时,通常使用它。Promise.all()
Promise.any()
# Promise.resolve() 和 Promise.reject()
当我们在定义一个 promise 的过程中,如果涉及到异步操作,那就需要通过new Promise
的方式创建一个 Promise 实例。
但有些场景下,我们并没有异步操作,但仍然想调用 promise.then,此时,我们可以用 Promise.resolve()
将其包装成成功的状态。同理,Promise.reject()
可以包装成失败的状态。
比如说,有的时候,promise 里面并不涉及异步操作,我只是单纯地想通过 promise 对象返回一个字符串(有的业务就是有这样的需求),那就可以通过 Promise.reslove('字符串')
Promise.reject('字符串')
、这种简写的方式返回。
这两种情况,我们来对比看看。
例 1:
function foo(flag) {
if (flag) {
return new Promise((resolve) => {
// 这里可以做异步操作
resolve('success');
});
// return Promise.resolve('success2');
} else {
return new Promise((reslove, reject) => {
// 这里可以做异步操作
reject('fail');
});
}
}
// 执行 reslove 的逻辑
foo(true).then((res) => {
console.log(res);
});
// 执行 reject 的逻辑
foo(false).catch((err) => {
console.log(err);
});
例 2:(见证奇迹的时刻)
function foo(flag) {
if (flag) {
// Promise的静态方法:直接返回字符串
return Promise.resolve('success');
} else {
// Promise的静态方法:直接返回字符串
return Promise.reject('fail');
}
}
// 执行 reslove 的逻辑
foo(true).then((res) => {
console.log(res);
});
// 执行 reject 的逻辑
foo(false).catch((err) => {
console.log(err);
});
例 1 和例 2 的打印结果是一样的。这两段代码的区别在于:例 1 里面可以封装异步任务;例 2 只能单纯的返回一个字符串等变量,不能封装异步任务。
# Promise.all()
Promsie.all([p1, p2, p3])
:并发处理多个异步任务,所有任务都执行成功,才算成功(才会走到 then);只要有一个任务失败,就会马上走到 catch,整体都算失败。参数里传的是 多个 promise 实例组成的数组。
# 语法举例
1、异步任务都执行成功时:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise1');
resolve('promise 1 成功');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise2');
resolve('promise 2 成功');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise3');
resolve('promise 3 成功');
}, 3000);
});
Promise.all([promise1, promise2, promise3])
.then((res) => {
// 三个异步任务都执行成功,才会走到这里
// 这里拿到的 res,是三个成功的返回结果组成的数组
console.log(JSON.stringify(res));
})
.catch((err) => {
// 只要有一个异步任务执行失败,就会马上走到这里
console.log(err);
});
打印结果:
// 1秒后
执行 promise1
// 2秒后
执行 promise2
// 3秒后
执行 promise3
["promise 1 成功","promise 2 成功","promise 3 成功"]
2、异步任务有至少一个执行失败时:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise1');
resolve('promise 1 成功');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise2');
// 这里通过 reject() 的方式,表示任务执行失败
reject('promise 2 失败');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise3');
resolve('promise 3 成功');
}, 3000);
});
Promise.all([promise1, promise2, promise3])
.then((res) => {
// 三个异步任务都执行成功,才会走到这里
console.log('走到 then:' + JSON.stringify(res));
})
.catch((err) => {
// 只要有一个异步任务执行失败,就会马上走到这里
console.log('走到 catch:' + err);
});
打印结果:
// 1秒后
执行 promise1
// 2秒后
执行 promise2
走到 catch:promise 2 失败
// 3秒后
执行 promise3
可以看到,当 promise2 执行失败之后,马上就走到了 catch,而且 promise3 里的 resolve 并没有执行。
# Promise.all()举例:多张图片上传
比如说,现在有一个图片上传的接口,每次请求接口时只能上传一张图片。需求是:当用户连续上传完九张图片(正好凑齐九宫格)之后,给用户一个“上传成功”的提示。这个时候,我们就可以使用Promsie.all()
。
代码举例如下:
const imgArr = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg', '8.jpg', '9.jpg'];
const promiseArr = [];
imgArr.forEach((item) => {
const p = new Promise((resolve, reject) => {
// 在这里做图片上传的异步任务。图片上传成功后,接口会返回图片的 url 地址
// upload img ==> return imgUrl
if (imgUrl) {
// 单张图片上传完成
resolve(imgUrl);
} else {
reject('单张图片上传失败');
}
});
promiseArr.push(p);
});
Promise.all(promiseArr)
.then((res) => {
console.log('图片全部上传完成');
console.log('九张图片的url地址,组成的数组:' + res);
})
.catch((res) => {
console.log('部分图片上传失败');
});
上方代码解释:
1、只有九张图片都上传成功,才会走到 then。
2、按时间顺序来看,假设第一张图片上传成功,第二张图片上传失败,那么,最终的表现是:
对于前端来说,九张图都会走到 reject;整体会走到 catch,不会走到 then。
对于后端来说,第一张图片会上传成功(因为写入 DB 是不可逆的),第二张图上传失败,剩下的七张图,会正常请求 upload img 接口。
3、特别说明:
第一张图会成功调 upload 接口,并返回 imgUrl,但不会走到 resolve,因为要等其他八张图的执行结果,再决定是一起走 resolove 还是一起走 reject。
当执行 Promise.all() / Promise.race() / Promise.any() 的时候,其实九张图的 upload img 请求都已经发出去了。对于后端来说,是没有区别的(而且读写 DB 的操作不可逆),只是在前端的交互表现不同、走到 resolve / reject / then / catch 的时机不同而已。
上面这个例子,在实际的项目开发中,经常遇到,属于高频需求,需要记住并理解。
4、思维拓展:
拓展 1:如果你希望九张图同时上传,并且想知道哪些图上传成功、哪些图上传失败,则可以这样做:无论 upload img 接口请求成功与否,全都执行 reslove。这样的话,最终一定会走到 then,然后再根据接口返回的结果判断九张图片的上传成功与否。
拓展 2:实战开发中,在做多张图片上传时,可能是一张一张地单独上传,各自的上传操作相互独立。此时
Promise.all
便不再适用,这就得具体需求具体分析了。
# Promise.race()
Promise.race([p1, p2, p3])
:并发处理多个异步任务,返回的是第一个执行完成的 promise,且状态和第一个完成的任务状态保持一致。参数里传的是多个 promise 实例组成的数组。
上面这句话,第一次读时,可能很绕口。我说的再通俗一点:在多个同时执行的异步任务中,先找出哪个异步任务最先执行完成(无论是走到 resolve,还是走到 reject,都算执行完成),整体的状态就跟这个任务保持一致。如果这个任务执行成功,那整体就算成功(走到 then);如果这个任务执行失败,那整体就算失败(走到 catch)。
race
的中文翻译,可以理解为“竞赛”。意思是,谁先抢到名额,就认定谁了。无论这个人最终的结局是成功或者失败,整体的结局,都以这个人的结局为准。
我刚开始学 Promise.race()的时候,误以为它的含义是“只要有一个异步执行成功,整体就算成功(走到 then);所有任务都执行失败,整体才算失败(走到 catch)”。现在想来,真是大错特错,过于懵懂。
现在我顿悟了,准确来说,Promise.race()强调的是:只要有一个异步任务执行完成,整体就是完成的。
Promise.race()的应用场景:在众多 Promise 实例中,最终结果只取一个 Promise,谁返回得最快就用谁的 Promise。
我们来看看各种场景的打印结果,便能擦干泪水,继续前行。
# 语法举例
场景 1、所有任务都执行成功时:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise1');
resolve('promise 1 成功');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise2');
resolve('promise 2 成功');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise3');
resolve('promise 3 成功');
}, 3000);
});
Promise.race([promise1, promise2, promise3])
.then((res) => {
// 第一个完成的任务,如果执行成功,就会走到这里
// 这里拿到的 res,是第一个成功的 promise 返回的结果,不是数组
console.log(JSON.stringify(res));
})
.catch((err) => {
// 第一个完成的任务,如果执行失败,就会走到这里
console.log(err);
});
打印结果:
// 1秒后
执行 promise1
"promise 1 成功"
// 2秒后
执行 promise2
// 3秒后
执行 promise3
场景 2、第一个任务成功、第二个任务失败时:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise1');
resolve('promise 1 成功');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise2');
// 第二个任务执行失败时
reject('promise 2 失败');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise3');
resolve('promise 3 成功');
}, 3000);
});
Promise.race([promise1, promise2, promise3])
.then((res) => {
// 第一个完成的任务,如果执行成功,就会走到这里
console.log('走到then:' + res);
})
.catch((err) => {
// 第一个完成的任务,如果执行失败,就会走到这里
console.log('走到catch:' + err);
});
打印结果:
// 1秒后
执行 promise1
走到then:promise 1 成功
// 2秒后
执行 promise2
// 3秒后
执行 promise3
可以看出,场景 2 的打印结果和场景 1 的打印结果,是一样的。因为最新执行完成的任务,是成功的,所以整体会马上走到 then,且整体就算成功。
场景 3、第一个任务失败、第二个任务成功时:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise1');
// 第一个任务执行失败时
reject('promise 1 失败');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise2');
resolve('promise 2 成功');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('执行 promise3');
resolve('promise 3 成功');
}, 3000);
});
Promise.race([promise1, promise2, promise3])
.then((res) => {
// 第一个完成的任务,如果执行成功,就会走到这里
console.log('走到then:' + res);
})
.catch((err) => {
// 第一个完成的任务,如果执行失败,就会走到这里
console.log('走到catch:' + err);
});
打印结果:
// 1秒后
执行 promise1
走到catch:promise 1 失败
// 2秒后
执行 promise2
// 3秒后
执行 promise3
看清楚了没?场景 3 的最终打印结果,是走到了 catch;任务 2 和任务 3 里的 resolve,并没有继续执行。
场景 3 的代码,一定好好好理解。
# Promise.race()举例:图片加载超时
现在有个需求是这样的:前端需要加载并显示一张图片。如果图片在三秒内加载成功,那就显示图片;如果三秒内没有加载成功,那就按异常处理,前端提示“加载超时”或者“请求超时”。
代码实现:
// 图片请求的Promise
function getImg() {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = function () {
// 图片的加载,是异步任务
resolve(img);
};
img.src = 'https://img.smyhvae.com/20200102.png';
});
}
// 加载超时的 Promise
function timeout() {
return new Promise((resolve, reject) => {
// 采用 Promise.race()之后,如果 timeout() 的 promise 比 getImg() 的 promise先执行,说明定时器时间到了,那就算超时。整体的最终结果按失败处理。
setTimeout(() => {
reject('图片加载超时');
}, 3000);
});
}
Promise.race([getImg(), timeout()])
.then((res) => {
// 图片加载成功
console.log(res);
})
.catch((err) => {
// 图片加载超时
console.log(err);
});
如代码注释所述:采用 Promise.race() 之后,如果 timeout() 的 promise 比 getImg() 的 promise 先执行,说明定时器时间到了,那就算超时。整体的最终结果按失败处理。
这个思路很巧妙。用同样的思路,我们还可以处理网络请求超时的问题。如果接口请求时长超过 3 秒,就按超时处理,也就是下面我们要举的例子。
# Promise.race()举例:网络请求超时
现在有这种需求:如果接口请求时长超过 3 秒,就按超时处理。
基于这种需求,我们可以用 Promise.race() 来实现:一个 Promise 用于 请求接口,另一个 Promise 用于执行 setTimeout()。把这两个 Promise 用 Promise.race()组装在一起,谁先执行,那么最终的结果就以谁的为准。
代码举例:
function query(url, delay = 4000) {
let promiseArr = [
myAajax(url),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('网络请求超时');
}, delay);
}),
];
return Promise.race(promiseArr);
}
query('http://localhost:8899/xxx_url', 3000)
.then((res) => {
console.log(res);
})
.catch((error) => {
console.log(error);
});
# 总结
Promise 不仅能解决嵌套异步任务的回调地域问题,也可做多个异步任务的并发请求,还可以进行舒适简洁的状态管理。
Promise 本身不是异步的,但是它可以封装异步任务,并对异步操作进行良好的状态管理,这便是 Promise 的魅力所在。