Promise
期约是对尚不存在结果的一个替身,它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。 这是红宝书中对promise的简短定义,ES6支持了Promises/A+规范,实现了 Promise 类型。
值与状态
我们使用Promise的时候,主要需要考虑它包装的值、状态。 任何一个 Promise对象都有一个私有的状态,不能直接获取到(避免以同步的方式改变Promise对象的状态)
- 待定(pending)
- 兑现(fulfilled / resolved)
- 拒绝(rejected)
new Promise
的时候必须填入一个 执行器函数 ,函数接受两个参数,常命名为 resolve()
和 reject()
,参数作为Promise的值。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。
Promise的状态一旦更改为fulfilled
,rejected
即不可撤销,再次调用resolve()
和 reject()
会静默失败。
Promise中的错误,使用同步的写法try/catch 是捕获不到的:Uncaught (in promise) Error: bar
,必须用Promise的方法以异步的方式捕获错误,即.then()的第二参数或者.catch()。
Promise.resolve()
这两个静态方法可以直接实例化 resolved
的Promise,第一个参数即为Promise的值,多余的参数会静默忽略。
Promise.resolve()
是一个幂等方法, 即:
let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true
也就是说,等幂性会保留传入的Promise状态:
let p = new Promise(() => {});
// p是一个 pedding 的 promise
setTimeout(console.log, 0, p); // Promise <pending>
// 等幂性会保留传入的Promise状态 pedding,就算是resolve包裹,输出还是pedding
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
Promise.reject()
实例化一个拒绝的Promise并抛出一个异步错误 (这个错误不能通过 try/catch 捕获,而只能通过.then()的第二个参数或者.catch捕获,后文说)。
参数是拒绝理由,即Promise <rejected>: 参数 <any>
。
Promise.reject() 不是等幂的!传入一个Promise实例,返回的是一个新的rejected状态的promise,而传入的实例会变成拒绝理由,即 Promise <rejected>: Promise <resolved>
Promise.prototype.then()
最多接受两个参数,都是可选的,在期约分别进入fulfilled
和rejected
状态时执行。实际上.catch是第二个参数的语法糖,只接受一个处理函数作为参数,我就用语法糖的写法(.then只接受一个参数,.catch再接受一个参数)来分开叙述两个处理程序的特性.
这里先只谈论.then()只填写第一个参数的情况
接收: 上一个resolved
promise的值
返回: 通过 Promise.resolve()
包装上一个期约fulfilled
之后的值,返回一个新的Promise
新的 Promise 包装的值根据.then的处理函数参数的返回值,遵循如下几种case:
let p1 = Promise.resolve(1) // .then 包装 上一个Promise fulfilled的值,这里是 1
// 1. 如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined
let p2 = p1.then((res)=> {
// return res
})
// 2. 有返回值会通过 Promise.resolve()包装
let p3 = p1.then((res)=> {
return res + 1
})
// 3. 如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值
let p4 = p1.then()
setTimeout(console.log, 0, p2, p3, p4);
// Promise { <resolved> undefined } Promise { <resolved> 2 } Promise { <resolved> 1 }
由于是Promise.resolve()
包装,而上文也提到这个函数具有等幂性,那么如果返回值是一个Promise实例,将保留返回值Promise状态:
let p1 = Promise.resolve(1)
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
setTimeout(console.log, 0, p8, p9);
// Promise { <pending> } Promise { <rejected> undefined }
而抛出异常或者返回错误,则分别会返回rejected Promise 以及 resolved的包裹错误的Promise
let p1 = Promise.resolve(1)
let p10 = p1.then(() => { throw 'baz'; });
let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p10, p11);
// Promise { <rejected> 'baz' } Promise { <resolved> Error: qux }
// 后者不难想,因为Promise.resolved()包裹一个Error对象就是返回的 <resolved> Error: qux
总结一下: .then会返回一个新的Promise实例,新实例的值由处理函数的是否存在、是否return、return的值
确定;状态则通过Promise.resolved()
包裹返回值确定,特别的,如果在处理函数中 throw异常
,会返回一个 rejected Promise。
Promise.prototype.catch()
.catch()是一个语法糖,等价于.then(null, onRejected)
。
接收: 上一个 rejected
Promise的值(也就是,无论前面有几个.then().catch()
,当前.catch()接受上一个返回rejected Promise的.then或.catch
, 详见下文Promise连锁)
返回: 依然通过 Promise.resolve()
包装,返回一个新的Promise
总结一下: .catch()返回的新Promise规则同.then()一毛一样,区别只是接受上一个Promise rejected后的值。
Promise.prototype.finally()
接收: 无论期约的状态是解决还是拒绝都会触发,主要用于添加清理代码。
返回: 新的Promise实例,大多数情况下为父Promise的传递(状态与值都如此),在处理函数返回pending Promise / throw异常,就会返回 pending / rejected 的promise
reject行为
上面提到了一些会返回 rejected Promise 的 case,但是零零散散,这里下文中重要,下面再总结一下:
// 1. 执行器函数中 调用reject()
new Promise((resolve, reject) => reject(Error('foo'))); // Promise <rejected>: Error: foo
// 2. 执行器函数中 使用throw语句
new Promise((resolve, reject) => { throw Error('foo'); }); // Promise <rejected>: Error: foo
// 3. .then() .catch() 的处理函数中,使用 throw语句
Promise.resolve().then(() => { throw Error('foo'); }); // Promise <rejected>: Error: foo
// 4. 用Promise.reject()方法实例化一个错误
Promise.reject(Error('foo')); // Promise <rejected>: Error: foo
- 虽然reject的参数可以随便填写,但是优雅的方式是使用Error对象
- 用catch捕获异步错误,用同步的方式会抛出
Uncaught Error
Promise 连锁
来了来了,来到最激动人心的时候了,铺垫了那么多,终于到了平时的主要使用场景了,Promise的链式调用。
异步任务串行化
来看一个例子:前端接口调用时,可能会遇到前一个接口的返回值作为参数继续请求的场景。这就要求让每个后续Promise都等待之前的Promise,也就是串行化异步任务。
假设我们封装好了接口请求的函数,入参是请求的参数,调用后返回一个Promise,接口返回正常时resolved,反之rejected。
// 假设A、B、C接口都需前者的返回值作为后者的请求参数
// 这里虽然可以象征性的 setTimeout(), 简单起见也不添加了,直接模拟接口全部返回成功
const getA = () => Promise.resolve({foo: 'foo'})
const postB = (data) => Promise.resolve({bar: 'bar', ...data})
const postC = (data) => Promise.resolve({qux: 'qux', ...data})
getA()
.then(res => {
console.log(res)
return postB(res)
})
.then(res => {
console.log(res)
return postC(res)
})
.then(res => {
console.log(res)
})
// { foo: 'foo' }
// { bar: 'bar', foo: 'foo' }
// { qux: 'qux', bar: 'bar', foo: 'foo' }
在上面的代码中,让每个执行器函数都返回一个Promise实例,也就是封装好的接口Promise,那么根据上述.then()小结中所说,在执行器函数中返回一个Promise,那么就会由Promise.resolve()
包裹,根据它的等幂性,就还是把得到了想要参数后接口Promise实例返回了,以此类推就可以串行化的调用了。
如何捕获错误呢?
const getA = () => Promise.reject(Error('A timeout')) // Promise.resolve({foo: 'foo'})
const postB = (data) => Promise.reject(Error('B timeout')) // Promise.resolve({bar: 'bar', ...data})
const postC = (data) => Promise.resolve({qux: 'qux', ...data})
getA() // 返回 Promise: <rejected> Error: A timeout
.then(res => { // 不会运行
console.log(res)
return postB(res)
})
.then(res => { // 不会运行
console.log(res)
return postC(res)
})
.then(res => { // 不会运行
console.log(res)
})
.catch(e => { // 运行捕获错误
console.log(e) // Error: A timeout
})
上方的代码中,getA
和 postB
都抛出了 rejected 的 Promise实例,上文写过:
.then()接收: 上一个
resolved
promise 的值.catch()接收: 上一个
rejected
promise 的值 那么这里,getA返回了一个resolved promise,那么下面的then都不会接受这个promise,直到遇到catch,打印出错误Error: A timeout
, 这样,三个接口如果有一个请求错误,链式.then()调用中断,错误都会被最后的catch()捕获,达到了一种统一处理错误的目的。
不要再用回调地狱的思维写promise了!
如下这般,一夜回到解放前:
// 不推荐,不优雅,不链式的写法
getA()
.then(resA => {
console.log(resA)
postB(resA).then(resB=> {
console.log(resB)
postC(resB).then(resC=> {
console.log(resC)
})
})
})
// { foo: 'foo' }
// { bar: 'bar', foo: 'foo' }
// { qux: 'qux', bar: 'bar', foo: 'foo' }
好吧,虽然输出是一样的,但是地狱又来了,现象你在真实的使用场景下还要加入catch的判断,还要解析各种变量,层层嵌套的作用域。。。
thats all, thanks