工作后夯实promise和异步函数基础

320 阅读11分钟

前言:异步和同步之间的概念就不多述了,在网上这种文章比比皆是,也不是本篇要讲的重点。

1.创建promise(期约)

1.1 es6新增promise引用类型,可以通过new操作符来实例化,并且创建新promise实例时,需要传入一个执行器函数参数,如果不传就会抛出SyntaxError

let p = new Promise(() => {})
setTimeout(console.log,0,p); //Promise <pending>

2.promise有三种状态:

pending(待定)

fulfilled(解决)

rejected(拒绝)

2.1 三种状态的理解

pendingpromise最初始的状态代表尚未开始或者正在执行,在pending状态下,promise可以settled(落定)为代表成功的fulfilled状态,也可以是 代表没有成功的rejected状态,但是无论变为哪种状态都是不可逆的,而且也不能保证promise一定为脱离pending状态.

let p = new Promise((resolve, reject) => {
    resolve();
    reject();// 没有效果
})
setTimeout(console.log,0,p) // Promsie <resolved>

2.2 执行器函数控制状态的改变

由于promise的状态是私有的,所以只能在promise的执行器函数中改变状态,其中执行器函数主要的作用就是初始化promise的异步行为和控制状态的最终转化。其中控制promsie状态转化是通过执行器函数的两个函数参数来实现的,这两个参数通常命名为resolvereject,在执行器函数中调用resolve()就会把状态切换成resolved,调用reject()就会把状态切换成rejected

let p1 = new Promise((resolve,reject) => resolve());
setTimeout(console.log,0,p1); //Promise <resolved>
let p2 = new Promise((resolve,reject) => reject());
setTimeout(console.log,0,p2); //Promise <rejected>

需要注意的一点是在promise中执行器函数是同步执行的,原因是因为执行器函数是promise的初始化程序

new Promise(() => setTimeout(console.log,0,‘1’));
setTimeout(console.log,0,‘2’);
// 1
// 2

2.3 另一种写法

2.3.1 Promise.resolve()

promise并非是一定通过执行器函数才能转变为resolved或者rejected状态,通过调用Promise.resolve()静态方法,可以实例化一个resolved状态的promise,下面两个例子是等价的

let p1 = new Promise((resolve, reject) => resolve())
let p2 = Promise.resolve();
let p = Promise.resolve(2);
setTimeout(console.log,0,p); // promise <resolved> :2

如果传给resolve的参数的本来就是一个promise那就是返回这个promise(幂等逻辑)

2.3.2 Promise.reject()

和promise.resolve()类似,promise.reject()会实例化一个rejectedpromise,并抛出一个异步错误,(这个错误不能通过try/catch捕获,只能通过rejectd处理程序捕获),下面这两个promise实例是一样的

let p1 = new Promise((resolve, reject) => reject())
let p2 = 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对象,则这个对象会成为它返回的rejected的理由(即会当参数传给处理程序)

3.promise的实例方法

promise实例的方法是连接外部同步代码和内部异步代码的桥梁

3.1 Promise.prototype.then()

这个方法接受两个参数,一个是onResolved处理程序,另一个是onRejected处理程序。这两个参数都是可选的,onResolved处理程序会在状态变为resolved执行,onRejected处理程序会在状态变为rejected执行。

function onResovled(id) {
    setTimeout(console.log, 0, id, 'resolved')
}
function onRejected(id) {
    setTimeout(console.log, 0, id, 'rejected')
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000))
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000))

p1.then(() => onResovled('p1'), () => onRejected('p1'))
p2.then(() => onResovled('p2'), () => onRejected('p2'))

// 三秒后
// p1 resolved
// p2 rejected

注意:promise.prototype.then()方法返回的是一个新的promise实例,这个新promise实例基于onResovled处理程序(then的第一个函数参数)的返回值构建,(catch是基于onjected处理程序的返回值构建的)换句话说,处理程序的返回值会通过Promise.resolve()包装连生成新的promise,2(如果没有这个处理程序。promise.resolve()就会包装上一个promise变为resolved状态的返回的值),3(如果没有显式的返回语句,则promise就会包装默认的返回值undefined。)

 let p1 = Promise.resolve('foo')
 // 若调用then(),不传处理程序,则像2情况一样
 let p2 = p1.then();
 setTimeout(console.log,0,p2) // Promise <resolved> : foo

 let p3 = p1.then(() => {}); // 像情况3一样
 setTimeout(console.log,0,p3) // Promise <resolved> : undefined

如果有显式的返回值,则promise.resolve()会包装这个返回值。

let  p6 = p1.then(() => 'bar')
let  p6 = p1.then(() => Promise.resolve('bar'))
setTimeout(console.log,0,p6) // Promise <resolved>: bar

抛出异常会返回rejected的promise:

let p10 = p1.then(() => {throw:'baz'});
setTimeout(console.log,0,p10) // Promsise <rejected>: baz

抛出错误值不会触发rejected行为,而是会把错误对象包装在一个resolved的promise中

let p11 = p1.then(()=> Error("qux"))
setTimeout(console.log,0,p11) // Promise <resolved: Error: qux>

3.2 Promise.prototype.catch()

Promise.prototype.catch()方法用于给promise添加rejected处理程序,这个方法只接受一个参数:onRejected处理程序,事实上,这个方法就是一个语法糖,调用它相当于调用promise.prototype.then(null,onRejected),catch方法也返回一个新的promise实例,与then方法相同的是catch的返回值也是promise.resolve()包裹。

3.3 Promise.prototype.finally()

Promise.prototype.finally()方法用于给promise添加 onFinally 处理程序,这个处理程序在promise转换为resolvedrejected状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是resolved还是rejected,所以这个方法主要用于添加清理代码。

let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
setTimeout(console.log, 0, 'Finally!')
}

p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally

then,catch方法一样,finally方法也会返回一个新的promise实例: 这个新期约实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态 无关的方法,所以在大多数情况下它将表现为上一个promise的传递。对于resolved状态和rejected状态都是如此。(对比上面放大的注意)

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('qux'));

setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo

image.png

4.传递解决值和拒绝理由

promise状态变为resolved时,就会有一个私有的内部值,当状态变为rejected时,就会有一个私有的内部理由,无论是值还是理由,都是包含原始内值或对象的不可修改的引用.

到了落定状态后,promise会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序。在执行函数中,解决的值和拒绝的理由是分别作为 resolve()reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数(then方法的第一个和第二个参数)。下面的例子展示了上述传递过程:

值传递(resolve,reject)

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar

拒绝理由的传递(throw Error)

在promise的执行函数或处理程序中抛出错误会导致状态变为rejected,对应的错误对象会成为拒绝的理由。因此以下这些promise都会以一个错误对象为由被拒绝.

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

setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo

注意:异步的错误不会阻塞同步代码的执行,且try/catch不能捕捉到promise抛出的错误。

另外:

then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之 后将其隔离,同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之 后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:

console.log('begin synchronous execution');
try {
throw Error('foo');
} catch(e) {
console.log('caught error', e);
}

console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution
new Promise((resolve, reject) => {

console.log('begin asynchronous execution');
reject(Error('bar'));
}).catch((e) => {
console.log('caught error', e);
}).then(() => {
console.log('continue asynchronous execution');
});

// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

5. Promise.all()和 Promise.race()

Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。 而合成后期约的行为取决于内部期约的行为。

5.1 Promise.all()

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个 可迭代对象,返回一个新期约:

let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);

setTimeout(console.log, 0, p); // Promise <pending>
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约 1 秒后)

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:

// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject(),
Promise.resolve()
]);

setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined

如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:

let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);

p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]

如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期 约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。合成的期约会静默 处理所有包含期约的拒绝操作,如下所示:

// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉

let p = Promise.all([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

5.2 Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个 方法接收一个可迭代对象,返回一个新期约:

let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
]);

// 可迭代对象中的元素会通过 Promise.resolve()转换为期约


Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的 期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:

// 解决先发生,超时后的拒绝被忽略

let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

setTimeout(console.log, 0, p1); // Promise <resolved>: 3

// 拒绝先发生,超时后的解决被忽略

let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);

setTimeout(console.log, 0, p2); // Promise <rejected>: 4

// 迭代顺序决定了落定顺序

let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)

]);

setTimeout(console.log, 0, p3); // Promise <resolved>: 5

如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约 不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。与 Promise.all() 类似,合成的期约会静默处理所有包含期约的拒绝操作,如下所示:

// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉

let p = Promise.race([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3

// 没有未处理的错误

总结:

创建一个promise实例,需要传入一个执行器函数,执行器的函数两个参数分别命名为resolve和reject,在执行器里去调用这两个参数,转换promise的状态,调用resolve()状态变为resolved,调用reject()状态变为rejected.也可以直接将状态转变,例如调用promise的静态方法promise.resolve()直接将状态变为resolved,调用promise.reject()状态变为rejected。

在实际用法里我们不可能是这样用的,一般会搭配promise实例方法(then,catch,finally)使用,来操作执行器函数执行之后得出的值,或者是错误。promise实例方法都会返回新的promise实例,then和catch都是通过promise.resolve()包裹对应处理程序的返回值(实际上就是将返回值作为resolve的参数),而finally就是将上一个promise的返回值用promise.resolve()去包裹,另外对新加的race,all静态方法都会返回一个新的promise实例,对于all可以这样认为等待参数中所有的promise都resolve时,返回的promise实例才会resolved,并且接受所有的promise的solved()参数数组作为新的promise的resolve的参数,如果有一个拒绝那么返回的新promise也会拒绝,拒绝的理由就是第一个接受理由。对于race可以这样认为,Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约。