前言:异步和同步之间的概念就不多述了,在网上这种文章比比皆是,也不是本篇要讲的重点。
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 三种状态的理解
pending
是promise
最初始的状态代表尚未开始或者正在执行,在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
状态转化是通过执行器函数的两个函数参数来实现的,这两个参数通常命名为resolve
和reject
,在执行器函数中调用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()会实例化一个rejected
的promise
,并抛出一个异步错误,(这个错误不能通过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转换为resolved
或rejected
状态时都会执行。这个方法可以避免 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
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()就会包装其解决值或拒绝理由并返回新期约。