深刻认识Promise

期约基础

ECMAScript6新增的引用类型Promise,可以通过new操作符来实例化,创建新期约的时候要传入一个函数参数,也叫执行器函数。如果不传入执行器函数,就会报SyntaxError错误。

let p = new Promise((resolve, reject) => {})
//执行器函数有两个参数
复制代码

一个新的期约实例有三种状态,待定(pending)、兑现或者叫解决(fulfilled或resolved)、拒绝(rejected)。待定是期约最初始的状态,在待定的状态下,期约可以转为解决或者拒绝状态。无论转换为哪一种转态都是不可逆的,而且,也不能保证期约必定会从待定转换为其他状态。所以代码逻辑中最好要考虑到所有的情况。
期约的状态是私有的,不能被外部的js代码修改,期约故意将异步行为封装起来,从而隔离外部的同步代码。

通过执行函数控制期约状态

由于期约的状态是私有的,所以只能在内部进行操作,内部操作在执行器函数中完成,执行器函数主要有两种职责:初始化期约的异步行为和控制状态的最终转换。控制状态的最终转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为resolve和reject。调用resolve会把状态切换为解决,调用reject会把状态切换为拒绝,也会抛出错误。

let p = new Promise((resolve, reject) => resolve())
let p = new Promise((resolve, reject) => reject())
复制代码

注意:执行器函数中的代码是同步代码。在执行器函数中无论resolve和reject哪个被调用,状态转换都是不可撤销的,如果继续修改状态会静默失败。

Promise.resolve()

期约通过调用执行器函数才能转换为落定状态,通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:

let p = new Promise((resolve, reject) => resolve());
let p1 = Promise.resolve();  
//这个解决的期约的值对应着传给Promise.resolve()的第一个参数,使用这个静态方法,实际上可以把任何值都转换为一个期约。
//多余的参数会被忽略
//对于这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似一个空包装。
let p = Promise.resolve(1);
setTimeout(console.log, 0, p === Promise.resolve(p));//true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));//true

//这个空包装也会保留传入期约的状态:
let p = new Promise(() => {});
setTimeout(console.log, 0, p); //Promise<pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); //true

//注意:这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此,也可能导致不符合预期的行为。
let p = Promise.resolve(new Error('abc'));
setTimeout(console.log, 0, p); //Promise<resolved>: Error: abc
复制代码

Promise.reject()

与Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误,这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获。下面的两个期约实例实际上是一样的

let p = new Promise((resolve, reject) => reject());
let p1 = Promise.reject();  
//这个拒绝的期约的理由就是传给Promise.reject()的第一个参数,这个参数也会传给后续的拒绝处理程序

let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise<rejected>: 3
p.then(null, (e) => {
  setTimeout(console.log, 0, e); //3
})
//注意的是,Promise.reject()并没有照搬Promise.resolve()传入的参数是期约的时候的处理方法,如果给Promise.reject()传入一个期约对象,则这个期约会成为它返回的拒绝期约的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve())); // Promise<rejected>: Promise<resolved>

复制代码

拒绝期约的错误只能通过异步方式捕获:

try {
  throw new Error('foo');
} catch (e) {
  console.log(e); // Error: foo
}

try {
  Promise.reject(new Error('bar'))
} catch (e) {
  console.log(e); // Uncaught(in promise) Error: bar
}
复制代码

期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁,这些方法可以访问异步操作返回的数据,处理期约成功和失败的输出,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。
Promise.prototype.then(): 这个方法接受最多两个参数,分别是onResolved处理程序和onRejected处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入解决和拒绝的状态时执行。因为期约只能转换一次,所以这两个操作一定是互斥的。如果传给then方法的参数不是函数的话,会被静默忽略。如果只想提供onRejected参数,那最好就要在onResolved参数的位置上传入null。这个then方法会返回一个新的期约实例。这个新期约实例会按照情况分别基于解决处理程序或者拒绝处理程序构建,换句话说,处理程序的返回值会通过Promise.resolve()包装来生成新期约。在期约解决的时候,如果没有提供解决处理程序,则Promise.resolve()就会包装上一个期约解决之后的值。如果没有显式的返回语句,则Promise.resolve会包装默认的返回值undefined。

//注意:为了方便表示,默认所有的console.log都是在setTimeout函数中打印的。
let p1 = Promise.resolve('foo');
let p2 = p1.then();
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());

console.log(p2); // Promise <resolved> : foo
console.log(p3); // Promise <resolved> : undefined
console.log(p4); // Promise <resolved> : undefined
console.log(p5); // Promise <resolved> : undefined

// 如果有显示的返回值,则Promise.resolve会包装这个值。

let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));
let p8 = p1.then(() => null);

console.log(p6); // Promise <resolved> : bar
console.log(p7); // Promise <resolved> : bar
console.log(p8); // Promise <resolved> : null

let p9 = p1.then(() => new Promise(() => {}));
let p10 = p1.then(() => Promise.reject());

console.log(p9); // Promise <pending>
console.log(p10); // Promise <rejected> : undefined

let p11 = p1.then(() => { throw 'baz' }); // 抛出异常会返回拒绝的期约。
console.log(p11); // Promise <rejected> : baz

// 返回错误值,不会抛出拒绝的期约,而是会把这个错误对象包装在一个解决的期约中。

let p12 = p1.then(() => Error('abc'));
console.log(p12); // Promise <resolved> : Error: abc
复制代码

onRejected处理程序也与之类似:onRejected处理程序的返回值也会被Promise.resolve()包装,乍一看,这可能会有点违反直觉,但是仔细想下的话,onRejected处理程序的任务不就是捕获异步的错误么,因此onRejected处理程序在捕获错误后不抛出异常是符合期约的行为的,应该返回一个解决的期约。

//注意:为了方便表示,默认所有的console.log都是在setTimeout函数中打印的。
let p1 = Promise.reject('foo');
let p2 = p1.then();
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());

console.log(p2); // Promise <rejected> : foo
console.log(p3); // Promise <resolved> : undefined
console.log(p4); // Promise <resolved> : undefined
console.log(p5); // Promise <resolved> : undefined

// 如果有显示的返回值,则Promise.resolve会包装这个值。

let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));
let p8 = p1.then(null, () => null);

console.log(p6); // Promise <resolved> : bar
console.log(p7); // Promise <resolved> : bar
console.log(p8); // Promise <resolved> : null

let p9 = p1.then(null, () => new Promise(() => {}));
let p10 = p1.then(null, () => Promise.reject());

console.log(p9); // Promise <pending>
console.log(p10); // Promise <rejected> : undefined

let p11 = p1.then(null, () => { throw 'baz' }); // 抛出异常会返回拒绝的期约。
console.log(p11); // Promise <rejected> : baz

// 返回错误值,不会抛出拒绝的期约,而是会把这个错误对象包装在一个解决的期约中。

let p12 = p1.then(null, () => Error('abc'));
console.log(p12); // Promise <resolved> : Error: abc
复制代码

Promise.prototype.catch():

Promise.prototype.catch()方法用于给期约添加拒绝处理程序,这个方法只接受一个参数:onRejected处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)。它会返回一个新的期约实例。

let p = Promise.reject();
let fun = function () {
  console.log('1');
}
p.then(null, fun);
p.catch(fun);
复制代码

Promise.prototyep.finally():

Promise.prototyep.finally()方法用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行,这个方法可以避免解决处理程序和拒绝处理程序出现冗余的代码,但是它不能知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。这个新期约的实例不同于then和catch方式返回的实例,它被设计为一个状态无关的方法,所以多数情况下它都会原样后传父期约,无论父期约是解决还是拒绝,都会原样后传。

let p1 = Promise.resolve('foo');
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('bar'));
// 以上这些打印都会是Promise<resolved>: foo

// 注意:如果返回的是一个待定的期约,或者onFinally处理程序抛出了错误(显示抛出或者返回了一个拒绝期约),则会返回一个相应的期约(待定或拒绝)。

let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
let p11 = p1.finally(() => { throw 'bar' });

console.log(p9); // Promise <pending>
console.log(p10); // Promise <rejected>: undefined
console.log(p11); // Promise <rejected>: bar
复制代码

传递解决值和拒绝理由

当期约到达落定状态后,期约会提供其解决值或者拒绝理由给相关状态的处理程序。当拿到上一步的返回值后,就可以进一步的对这个值进行处理。比如,第一次网络请求返回的数据是第二次请求的参数值,那么第一次请求返回的值就应该传给解决处理程序继续处理。失败的网络请求则需要传给拒绝处理程序处理。注意:期约解决的时候传递的值只有解决处理程序能接收,期约拒绝的时候传递的值只有拒绝处理程序能接收。多个解决处理程序的后面只需要跟一个拒绝处理程序即可。如下图:

image.png

image.png

拒绝期约和拒绝错误处理

拒绝期约类似于throw()表达式,因为他们都代表一种程序状态,即需要中断或者特殊处理,在期约的执行函数中或处理程序中抛出错误都会导致拒绝,对应的错误对象会成为拒绝的理由。

let p1 = new Promise((resolve, reject) => {
  reject(Error('foo'));
})
let p2 = new Promise((resolve, reject) => {
  throw Error('foo');
})
let p3 = Promise.resolve().then(() => {
  throw Error('foo');
})
let p4 = Promise.reject(Error('foo'));

以上四种情况都会导致期约变成拒绝,Promise <rejected>: Error: foo
复制代码

注意:期约可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,因为创建错误对象可以让浏览器捕获对象中的栈追踪信息,而这些信息对调试是非常关键的。在使用throw抛出错误的时候,js的运行时错误处理机制会停止执行抛出错误之后的任何指令。但是在期约中使用reject处理错误的时候,错误是异步抛出的,不会阻止其他代码的执行。

Promise.all()

该方法接收一组期约,只有所有的期约都解决了该方法创建的期约才会解决。这个静态方法接受一个可迭代对象,返回一个新期约。

let p1 = Promise.all([
  Promise.resolve(),
  Promise.resolve(),
])
// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.all([3,4]);
//空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([]);
// 无效语法
let p4 = Promise.all();

//如果所有的期约都成功解决,则合成期约的解决值就是包含期约解决值的数组,按照迭代器中出现的顺序排列。
let p = Promise.all([
  Promise.resolve(1),
  Promise.resolve(),
  Promise.resolve(3),
])
p.then(values => {
  console.log(values); // [1, undefined, 3]
})

//如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后再拒绝的期约不会
//影响最终期约的拒绝理由,不过这并不影响所有包含期约正常的拒绝操作,合成的
//期约会静默处理所有包含期约的拒绝操作,不会有抛出的未解决的错误。
复制代码

Promise.race()

该方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像,接受一个可迭代对象,返回一个新的期约。

let p1 = Promise.race([
  Promise.resolve(),
  Promise.resolve(),
])
setTimeout(() => {
  console.log(p1); // Promise <resolved> : undefined
}, 0);

// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.race[3, 4];
setTimeout(() => {
  console.log(p2); // Promise <resolved> : 3
}, 0);

// 空的可迭代对象等价于new Promise(() => {})
let p3 = Promise.race([]);

// 无效的语法
let p4 = Promise.race();
复制代码

注意:Promise.race方法不会区别对待解决或者拒绝的期约,无论是解决还是拒绝,只要是第一个落定的期约,Promise就会包装其解决值或拒绝理由并返回新期约。如果有一个期约拒绝并且它是第一个落定的,就会成为Promise.race()返回期约的拒绝理由,之后再拒绝的期约不会影响最终期约的拒绝理由,不过这并不影响所有包含期约正常的拒绝操作,与Promise.all()类似,Promise.race()也会静默处理所有包含期约拒绝的操作,不会有抛出未捕获的错误。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享