【JS】理解Promise : 拒绝"Promise地狱"

381 阅读7分钟

Promise

期约是对尚不存在结果的一个替身,它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。 这是红宝书中对promise的简短定义,ES6支持了Promises/A+规范,实现了 Promise 类型。

值与状态

我们使用Promise的时候,主要需要考虑它包装的值、状态。 任何一个 Promise对象都有一个私有的状态,不能直接获取到(避免以同步的方式改变Promise对象的状态)

  • 待定(pending)
  • 兑现(fulfilled / resolved)
  • 拒绝(rejected)

new Promise的时候必须填入一个 执行器函数 ,函数接受两个参数,常命名为 resolve()reject(),参数作为Promise的值。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。

Promise的状态一旦更改为fulfilledrejected即不可撤销,再次调用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()

最多接受两个参数,都是可选的,在期约分别进入fulfilledrejected状态时执行。实际上.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
  })

上方的代码中,getApostB 都抛出了 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