一、Promise 探究
先来打印 Promise ,看一下它的结构组成。
1. Promise 是什么
由上可见,Promise 是一个构造函数,它有 reject、resolve、all、race 等静态方法,原型上有 then、catch 等方法,所以 new 的实例对象肯定有 then、catch 这些方法。
2. 先来 new 一个?
let p = new Promise(function(resolve, reject) {
// 做一些异步操作
setTimeout(() => {
console.log('执行完成');
resolve('随便什么数据');
}, 1000);
});
// 1s 后,输出'执行完成'
Promise 的参数是一个函数,该函数会传入两个参数:resolve、reject,可以将 Promise 状态变为 fulfilled 和 rejected。
在上面的代码中,我们执行了一个异步操作 setTimeout,然后 1s 后,输出“执行完成”,并调用了 resolve方法。注意:虽然仅仅是 new 了一个对象,并没有调用参数函数,但该函数已经执行了。即 「new Promise() 和 执行参数函数 是同步的」,所以我们在用 Promise 的时候一般是外面再包一层函数,需要的时候再去调用,如:
function runAsync() {
let p = new Promise(function(resolve, reject) {
// 做一些异步操作
setTimeout(() => {
console.log('执行完成');
resolve('随便什么数据');
}, 1000);
});
return p; // 返回 promise实例
}
runAsync(); // 运行外部函数,间接执行 Promise
3. Promise.prototype.then
在执行 runAsync函数 后,我们会得到一个 Promise实例,实例上肯定有 then、catch 方法。
runAsync().then(function(data) {
console.log(data);
});
// 1s 后输出 “执行完成”、紧接着输出 “随便什么数据”
在返回的 promise 实例上直接调用 then 方法。then 方法接收的第一个参数是函数,该函数可以拿到我们在 runAsync 函数中调用resolve时传的参数。
4. Promise 和回调函数的对应关系
如果 函数f1 是异步操作并且 函数f2 又必须等到 f1 执行完成,那么我们之前采用的是传统形式的回调函数 f1(f2)。(f2是回调函数)
而对于 Promise 来说,我们将异步操作放到了 Promise 参数中。等到异步操作结束后,通过 resolve 或 reject 来改变Promise对象的状态,之后依据状态去执行 then 里的回调函数(或catch里的回调函数)。
- 异步操作
f1:把异步操作放到了 new Promise() 的参数函数中。 - 回调函数
f2:then 里面的函数。
function runAsync() {
let p = new Promise(function(resolve, reject) {
// 做一些异步操作
setTimeout(() => {
console.log('执行完成');
resolve('随便什么数据');
}, 1000);
});
return p;
}
runAsync().then(function(data) {
console.log(data);
});
5. 为什么要用 Promise:链式调用
我们可以看到,Promise 其实就是把传统形式的回调函数分离出来,在异步操作执行完后,用链式调用的方式来执行回调函数。像上面这个函数,我们也可以用传统回调去写,如下:
function runAsync(callback) {
setTimeout(() => {
console.log('执行完成');
callback('随便什么数据');
}, 1000);
}
runAsync(function(data) {
console.log(data);
});
但是当出现多层回调的时候,或者当回调函数也是一个异步操作,并且执行完后也有对应的回调函数的时候,我们怎么处理呢?总不能定义多个callback传进去吧,这样会使得程序结构混乱、流程难以追踪。而 Promise 的优势就在于,它可以在 then 方法中返回下一个异步操作的 promise实例,然后继续调用 then 来执行回调函数。
runAsync1()
.then(function(data) {
console.log(data);
return runAsync2();
})
.then(function(data) {
console.log(data);
});
// 1执行完成
// 随便什么数据1
// 2执行完成
// 随便什么数据2
6. then方法的返回值
then 方法不仅可以 return promise实例,还有以下几种情况:
- return 1 个值,那么 then 返回的promise状态会变为 fulfilled,并将返回值作为回调的参数。
- 没有任何返回值,那么 then 返回的promise...也是 fulfilled,并且后面回调的参数是 undefined。
- 抛出一个错误,那么...rejected,并将抛出的错误作为回调参数值。
runAsync1()
.then(function(data) {
console.log(data);
return runAsync2();
})
.then(function(data) {
console.log(data);
return '直接返回数据';
})
.then(function(data) {
console.log(data);
});
// 1执行完成
// 随便什么数据1
// 2执行完成
// 随便什么数据2
// 直接返回数据
7. catch
catch 方法用来指定 reject 的回调。
runAsync()
.then(function(data) {
console.log(data);
})
.catch(function(reason) {
console.log(reason);
});
catch 还有另外一个作用:在执行 fulfilled 的回调函数时,如果抛出异常,那么并不会报错卡死js,而是会进到后面的 catch 方法中,如下
runAsync()
.then(function(data) {
console.log(data);
console.log(x); // 没有定义x
})
.catch(function(reason) {
console.log(reason);
});
// 将错误原因传给了reason参数
二、Promise 基础概念
1. Promise 对象的状态
Promise 对象有 3 种状态:pending 异步操作未完成,fulfilled 异步操作成功,rejected 异步操作失败。并且只有 2 种变化的方式:pending -> fulfilled 和 pending -> rejected。
通过JS引擎内置的 resolve() 和 reject() 两个函数实现状态的改变(不是Promise的静态方法),如下:
- 异步操作成功时调用
resolve函数,Promise实例传回一个值,状态变为fulfilled。 - 异步操作失败时调用
reject函数,Promise实例抛出一个错误,状态变为rejected。
resolve 函数的参数除了正常的值外,还可能是另一个 promise实例,此时状态取决于参数实例的状态,自己的状态无效了。
let p1 = new Promise(function (resolve, reject) {
// ...
});
let p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
});
// p1 的状态决定了 p2 的状态
调用 resolve 或 reject 函数并不会终结 Promise 的参数函数的执行。 但一般来说,调用 resolve 或 reject 后,Promise的使命就完成了,后续操作应该放到 then 方法里面,因此最好在 resolve/reject 函数前面加上 return 语句。
new Promise((resolve, reject) => {
resolve(1); // 推荐做法: return resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2 1
另外,需要牢记的是:
- 对象的状态不受外界影响,只有异步操作的结果才能决定 Promise 的状态。
- 一旦状态改变,就不会再变。
2. Promise.prototype.then /catch
1. then方法和链式调用
then 方法的作用是为 Promise 实例添加状态改变时的回调函数,两个参数(均可选)分别是 异步操作成功(状态变为 fulfilled)的回调函数,和 异步操作失败(状态变为 rejected)的回调函数,可省略第 2 个参数,他们都接受实例传出的值作为参数。
new Promise((resolve, reject) => {
resolve(1);
})
.then(() => {
console.log(2);
})
.then(() => console.log(3), (v) => console.log(v));
// 2 3
2. 怎么中断链式调用? 可以在某个异步操作中添加:
throw 'break':抛出异常,则后面的Promise新建 和 链式调用 都中断。return new Promise((resolve, reject) => {})让实例永远处于 pending状态,中断链式调用。Promise.reject,中断链式调用。
3. catch
catch 方法和 then 的第二个参数一样,用来指定 reject 的回调函数,它实际是 obj.then(undefined, fn) 的语法糖。
因此如下,catch的返回值也像 then 看齐,在没有返回值时,catch 返回的 promise 状态会变为 fulfilled, 执行 then 对应的回调,传参值为 undefined。
new Promise((resolve, reject) => {
reject(1);
})
.catch(() => {
console.log(2);
})
.then(() => console.log(3), (v) => console.log(v));
// 2 3
运行完 catch方法后,会接着运行后面的then方法指定的回调函数。
4. Promise错误处理
推荐使用 then...catch 的方式来定义回调函数,因为:
-
用catch 可以捕获前面 then 指定回调的错误。
-
Promise对象的错误会一直向后传递,直到被捕获为止。我们用catch 可以捕获前面的多个 promise对象的错误 (catch自身的错误,如果后面没有catch则不能被捕获,前面的catch管不了后面的then)。
-
如果catch返回的还是promise对象,那么它后面的then方法可以继续执行。当catch前面都没有error时,会跳过catch继续执行后面的then。
- 捕获错误的方式(不能用 try ...catch,因为它用于捕获同步操作的异常,try 中如果放异步操作,那么catch是捕获不到的)
- catch
- then 的第二个参数
3. all 的用法
all 方法把多个promise实例包装成了一个新的promise实例,其中 all 的参数是一个数组,数组成员都是Promise实例。
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
// 1执行完成
// 2执行完成
// 3执行完成
// ['随便什么数据1', '随便什么数据2', '随便什么数据3']
let p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,
- 只有p1、p2、p3 的状态都变成 fulfilled,p的状态才会变成 fulfilled。此时all 方法会在所有异步操作执行完后,将这多个异步操作返回的结果放到数组中传给p的回调函数。
- 只要其中任一实例状态变成 rejected,p的状态就会变为 rejected,此时并不会等待所有的异步操作执行完,而是立刻将第一个被 rejected 的实例的返回值,传递给 p 的回调函数执行。
Promise.all 方法提供了并行执行异步操作的能力,有了all,就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
4. race 的用法
race 和 all 用法一样,但 race 是只要p1、p2、p3 之中有一个实例率先改变状态,那么p的状态就跟着改变,并立刻将该实例的返回值传给p的回调函数。
并不会等待所有异步操作执行完成,同样,p 的回调函数执行也不影响其他异步操作实例执行。
【注意】Promise.all 和 Promise.race 都是并行的。
- 并行:多个异步请求同时执行。
- 串行:一个异步请求完成后再进行下一个请求。
5. Promise.resolve / reject:将现有对象转为promise对象
Promise.resolve
Promise.resolve('foo');
等价于
new Promise(resolve => resolve('foo'));
(1)对于不带参数的Promise.resolve()、参数是原始值的Promise.resolve('hello')、不具有then方法的对象,那么Promise.resolve会直接返回一个resolved状态的promise对象,对应的回调函数会作为微任务在本轮事件循环结束时执行。
(2)对于参数就是promise对象的,那么Promise.resolve(p) 不会作任何修改,直接返回该实例。
(3)如参数是一个具有then方法的对象,那么会将这个对象转成Promise对象,然后立即执行该对象中的then方法。then方法执行后,p1的状态就变为resolved,再执行最后的then方法。
Promise.reject返回一个新的 promise实例,该实例的状态为rejected。
6. 其他方法
-
实例方法
finally不管Promise对象状态如何,都会执行的操作。它的回调函数不接受任何参数。 -
静态方法
allSettled不管每一个异步操作是成功和失败, allSettled 方法都会等所有的异步操作都结束了,再进行下一步操作。参数和返回值的形式类似于 all 方法。并且新的promise实例只要发生状态变更,那么一定是变成fulfilled。 -
静态方法
any和 all 更好相反,只要一个变为 fulfilled,新实例状态就变为 fulfilled;只有当所有的都变成 rejected,那么包装实例才会变成 rejected。
7. 设计思想和优缺点
设计思想:所有的异步操作都返回一个 Promise 实例,然后使用 then 方法指定下一步的回调函数。
优点:避免了层层嵌套的回调函数。
缺点:
- 无法取消 Promise,一旦 new 创建了就会立即执行,无法中途取消。
- 与 try...catch 不同,如果没有设置catch等处理错误的回调函数,那么Promise抛出的错误,不会反应到外部,也就是没有任何反应。
- 当处于 pending状态时,不能知道目前的进展是刚刚开始还是即将完成。
三、Promise/A+ 规范
参考文章