关注前端小讴,阅读更多原创技术文章

期约

  • 期约是对尚不存在结果的一个替身,是一种异步程序执行的机制

相关代码 →

Promises/A+规范

  • ES6 新增了Promise类型,其成为主导性的异步编程机制,所有现代浏览器都支持期约

期约基础

  • Promise类型通过new操作符实例化,需传入执行器(executor)函数作为参数
// let p = new Promise() // TypeError: Promise resolver undefined is not a function,必须提供执行器函数作为参数
let p = new Promise(() => {})
setTimeout(console.log, 0, p) // Promise { <pending> }

期约状态机

  • 期约是一个有状态的对象:

    • 待定pending表示尚未开始或正在执行。最初始状态,可以落定为兑现或拒绝状态,兑现后不可逆
    • 兑现fullfilled(或解决resolved)表示成功完成
    • 拒绝rejected表示没有成功完成
  • 期约的状态是私有的,其将异步行为封装起来隔离外部代码,不能被外部 JS 代码读取或修改

解决值、拒绝理由及期约用例

  • 期约的状态机可以提供很有用的信息,假设其向服务器发送一个 HTTP 请求:

    • 返回 200-299 范围内的状态码,可让期约状态变为“兑现”,期约内部收到私有JSON字符串,默认值为 undefined
    • 返回不在 200-299 范围内的状态码,会把期约状态切换为“拒绝”,期约内部收到私有Error对象(包含错误消息),默认值为 undefined

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

  • 期约的状态是私有的,只能在执行器函数中完成内部操作
  • 执行器函数负责初始化期约异步行为控制状态转换

    • 通过resolve()reject()两个函数参数控制状态转换
    • resolve()会把状态切换为兑换reject()会把状态切换为拒绝抛出错误
let p1 = new Promise((resolve, reject) => resolve())
setTimeout(console.log, 0, p1) // Promise {<fulfilled>: undefined}

let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log, 0, p2) // Promise {<rejected>: undefined}
// Uncaught (in promise)
  • 执行器函数是期约的初始化程序,其是同步执行的
new Promise(() => setTimeout(console.log, 0, 'executor'))
setTimeout(console.log, 0, 'promise initialized')
/* 
  'executor',先打印
  'promise initialized',后打印
*/
  • 可添加setTimeout推迟执行器函数的切换状态
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000)
})
setTimeout(console.log, 0, p3) // Promise { <pending> },打印实例p3时,还不会执行内部回调
  • resolve()reject()无论哪个被调用,状态转换都不可撤销,继续修改状态会静默失败
let p4 = new Promise((resolve, reject) => {
  resolve()
  reject() // 静默失败
})
setTimeout(console.log, 0, p4) // Promise {<fulfilled>: undefined}
  • 为避免期约卡在待定状态,可添加定时退出功能,设置若干时长后无论如何都拒绝期约的回调
let p5 = new Promise((resolve, reject) => {
  setTimeout(reject, 10000) // 10秒后调用reject()
})
setTimeout(console.log, 0, p5) // Promise { <pending> },10秒内,不调用resolve()
setTimeout(console.log, 11000, p5) // Promise {<rejected>: undefined},10秒外,调用reject()
// Uncaught (in promise)

Promise.resolve()

  • 调用Promise.resolve()方法,可以实例化一个解决的期约
let p6 = new Promise((resolve, reject) => {
  resolve()
})
console.log(p6) // Promise {<fulfilled>: undefined}
let p7 = Promise.resolve()
console.log(p7) // Promise {<fulfilled>: undefined}
  • 传给Promise.resolve()第一个参数为解决的期约的值
setTimeout(console.log, 0, Promise.resolve()) // Promise {<fulfilled>: undefined}
setTimeout(console.log, 0, Promise.resolve(3)) // Promise {<fulfilled>: 3}
setTimeout(console.log, 0, Promise.resolve(4, 5, 6)) // Promise {<fulfilled>: 4},只取首个参数
  • Promise.resolve()是一个幂等方法,如果传入的参数是一个期约,其行为类似于一个空包装
let p8 = Promise.resolve(7)
setTimeout(console.log, 0, Promise.resolve(p8)) // Promise { 7 }
setTimeout(console.log, 0, p8 === Promise.resolve(p8)) // true
setTimeout(console.log, 0, p8 === Promise.resolve(Promise.resolve(p8))) // true
  • 该幂等性会保留传入期约的状态
let p9 = new Promise(() => {}) // 待定状态
setTimeout(console.log, 0, p9) // Promise { <pending> }
setTimeout(console.log, 0, Promise.resolve(p9)) // Promise { <pending> }
setTimeout(console.log, 0, Promise.resolve(Promise.resolve(p9))) // Promise { <pending> }
  • 该方法能够包装任何非期约值(包括错误对象),并将其转换为解决的期约,因此可能导致不符合预期的行为
let p10 = Promise.resolve(new Error('foo'))
setTimeout(console.log, 0, p10) // Promise {<fulfilled>: Error: foo

Promise.reject()

  • Promise.resolve()相似,Promise.reject()可以实例化一个拒绝的期约并抛出一个异步错误

    • 该错误不能通过try/catch捕获,只能通过拒绝处理程序捕获
let p11 = new Promise((resolve, reject) => {
  reject()
})
console.log(p11) // Promise {<rejected>: undefined}
// Uncaught (in promise)

let p12 = Promise.reject()
console.log(p12) // Promise {<rejected>: undefined}
// Uncaught (in promise)
  • 传给Promise.resolve()第一个参数为拒绝的期约的理由,该参数也会传给后续的拒绝处理程序
let p13 = Promise.reject(3)
setTimeout(console.log, 0, p13) // Promise { <rejected> 3 }
p13.then(null, (err) => setTimeout(console.log, 0, err)) // 3,参数传给后续拒绝处理程序
  • Promise.reject()不是幂等的(与Promise.resolve()不同),如果参数为期约对象,则该期约会成为返回的拒绝理由
setTimeout(console.log, 0, Promise.reject(Promise.resolve())) // Promise {<rejected>: Promise}

同步/异步执行的二元性

  • 由于期约的异步特性,其虽然是同步对象(可在同步执行模式中使用),但也是异步执行模式的媒介

    • 同步线程的代码无法捕获拒绝的期约,拒绝期约的错误会通过浏览器异步消息队列来处理
    • 代码一旦开始以异步模式执行,唯一与之交互的方式就是使用异步结构,即期约
try {
  throw new Error('foo') // 同步线程抛出错误
} catch (error) {
  console.log(error + '1') // Error: foo1,同步线程捕获错误
}

try {
  Promise.reject('bar') // 同步线程使用期约
} catch (error) {
  console.log(error + '2') // 同步线程捕获不到拒绝的期约
}
// Promise {<rejected>: "bar"},浏览器异步消息队列捕获到拒绝的期约
// Uncaught (in promise) bar

期约的实例方法

  • 这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果

实现 Thenable 接口

  • ECMAScript 暴露的异步结构中,任何对象都有一个then()方法,该方法被认为实现了thenable接口
class MyThenable {
  // 实现Thenable接口的最简单的类
  then() {}
}

Promise.prototype.then()

  • Promise.prototype.then()为期约添加处理程序,接收2 个可选的处理程序参数onResolvedonRejected

    • onResolved会在期约进入兑现状态时执行
    • onRejected会在期约进入拒绝状态时执行
function onResolved(id) {
  setTimeout(console.log, 0, id, 'resolved')
}
function onRejected(id) {
  setTimeout(console.log, 0, id, 'rejected')
}
let p14 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000)
})
let p15 = new Promise((resolve, reject) => {
  setTimeout(reject, 3000)
})

p14.then(
  () => {
    onResolved('p14') // 'p14 resolved'(3秒后)
  },
  () => {
    onRejected('p14')
  }
)
p15.then(
  () => {
    onResolved('p15')
  },
  () => {
    onRejected('p15') // 'p15 rejected'(3秒后)
  }
)
  • 传给then()的任何非函数类型的参数都会被静默忽略(不推荐),如果只提供onResolvedonRejected,一般在另一个参数位置上传入nullundefined
p14.then('gobbeltygook') // 参数不是对象,静默忽略
p14.then(() => onResolved('p14')) // 'p14 resolved'(3秒后),不传onRejected
p15.then(null, () => onRejected('p15')) // 'p15 rejected'(3秒后),不传onResolved
  • Promise.prototype.then()返回一个新的期约实例,该实例基于onResolved处理程序(Promise.resolved()包装)的返回值构建

    • 没有提供这个处理程序,则包装上一个期约解决之后的值(父期约的传递)
    • 若提供了处理程序,但没有显示的返回语句,则包装默认的返回值 undefined
    • 若提供了处理程序,且有显示的返回值,则包装这个值
    • 若提供了处理程序,且返回期约,则包装返回的期约
    • 若提供了处理程序,且抛出异常,则包装拒绝的期约
    • 若提供了处理程序,且返回错误值,则把错误对象包装在一个解决的期约中(而非拒绝的期约)
let p16 = Promise.resolve('foo')

let result1 = p16.then() // 没有提供处理程序
setTimeout(console.log, 0, result1) // Promise {<fulfilled>: 'foo'},包装上一个期约解决后的值

let result2 = p16.then(() => undefined) // 处理程序没有显示的返回语句
let result3 = p16.then(() => {}) // 处理程序没有显示的返回语句
let result4 = p16.then(() => Promise.resolve()) // 处理程序没有显示的返回语句
setTimeout(console.log, 0, result2) // Promise {<fulfilled>: undefined},包装默认返回值undefined
setTimeout(console.log, 0, result3) // Promise {<fulfilled>: undefined},包装默认返回值undefined
setTimeout(console.log, 0, result4) // Promise {<fulfilled>: undefined},包装默认返回值undefined

let result5 = p16.then(() => 'bar') // 处理程序有显示的返回值
let result6 = p16.then(() => Promise.resolve('bar')) // 处理程序有显示的返回值
setTimeout(console.log, 0, result5) // Promise {<fulfilled>: 'bar'},包装这个值
setTimeout(console.log, 0, result6) // Promise {<fulfilled>: 'bar'},包装这个值

let result7 = p16.then(() => new Promise(() => {})) // 处理程序返回一个待定的期约
let result8 = p16.then(() => Promise.reject('bar')) // 处理程序返回一个拒绝的期约
// Uncaught (in promise) bar
setTimeout(console.log, 0, result7) // Promise {<pending>},包装返回的期约
setTimeout(console.log, 0, result8) // Promise {<rejected>: 'bar'},包装返回的期约

let result9 = p16.then(() => {
  throw 'baz' // 处理程序抛出异常
})
// Uncaught (in promise) baz
setTimeout(console.log, 0, result9) // Promise {<rejected>: 'baz'},包装拒绝的期约

let result10 = p16.then(() => Error('qux')) // 处理程序返回错误值
setTimeout(console.log, 0, result10) // Promise {<fulfilled>: Error: qux},把错误对象包装在一个解决的期约中
  • onResolved相同,onRejected处理程序作为参数时,其返回的值也被Promise.resolve()包装,返回新的期约实例
let p17 = Promise.reject('foo')

let result11 = p17.then() // 没有提供处理程序
// Uncaught (in promise) foo
setTimeout(console.log, 0, result11) // Promise {<rejected>: 'foo'},包装上一个期约解决后的值

let result12 = p17.then(null, () => undefined) // 处理程序没有显示的返回语句
let result13 = p17.then(null, () => {}) // 处理程序没有显示的返回语句
let result14 = p17.then(null, () => Promise.resolve()) // 处理程序没有显示的返回语句
setTimeout(console.log, 0, result12) // Promise {<fulfilled>: undefined},包装默认返回值undefined
setTimeout(console.log, 0, result13) // Promise {<fulfilled>: undefined},包装默认返回值undefined
setTimeout(console.log, 0, result14) // Promise {<fulfilled>: undefined},包装默认返回值undefined

let result15 = p17.then(null, () => 'bar') // 处理程序有显示的返回值
let result16 = p17.then(null, () => Promise.resolve('bar')) // 处理程序有显示的返回值
setTimeout(console.log, 0, result15) // Promise {<fulfilled>: 'bar'},包装这个值
setTimeout(console.log, 0, result16) // Promise {<fulfilled>: 'bar'},包装这个值

let result17 = p17.then(null, () => new Promise(() => {})) // 处理程序返回一个待定的期约
let result18 = p17.then(null, () => Promise.reject('bar')) // 处理程序返回一个拒绝的期约
// Uncaught (in promise) bar
setTimeout(console.log, 0, result17) // Promise {<pending>},包装返回的期约
setTimeout(console.log, 0, result18) // Promise {<rejected>: 'bar'},包装返回的期约

let result19 = p17.then(null, () => {
  throw 'baz' // 处理程序抛出异常
})
// Uncaught (in promise) baz
setTimeout(console.log, 0, result19) // Promise {<rejected>: 'baz'},包装拒绝的期约

let result20 = p17.then(null, () => Error('qux')) // 处理程序返回错误值
setTimeout(console.log, 0, result20) // Promise {<fulfilled>: Error: qux},把错误对象包装在一个解决的期约中

Promise.prototype.catch()

  • Promise.prototype.catch()为期约添加拒绝处理程序,接收1 个可选的处理程序参数onRejected

    • 该方法相当于调用Promise.prototypr.then(null, onRejected)
    • 该方法方法也返回新的期约实例,其行为与Promise.prototype.then()onRejeted处理程序一样
let p18 = Promise.reject()
let onRejected2 = function () {
  setTimeout(console.log, 0, 'reject')
}
p18.then(null, onRejected2) // 'reject'
p18.catch(onRejected2) // 'reject',两种添加拒绝处理程序的方式是一样的

Promise.prototype.finally()

  • Promise.prototype.finally()为期约添加 onFinally 处理程序,接收1 个可选的处理程序参数onFinally

    • 无论期约转换为解决还是拒绝状态,onFinally处理程序都会执行,但其无法知道期约的状态
    • 该方法主要用于添加清理代码
let p19 = Promise.resolve()
let p20 = Promise.reject()
let onFinally = function () {
  setTimeout(console.log, 0, 'Finally')
}
p19.finally(onFinally) // 'Finally'
p20.finally(onFinally) // 'Finally'
  • Promise.prototype.finally()返回一个新的期约实例,其以下情况均包装父期约的传递

    • 未提供处理程序
    • 提供了处理程序,但没有显示的返回语句
    • 提供了处理程序,且有显示的返回值
    • 处理程序返回一个解决的期约
    • 处理程序返回错误值
let p21 = Promise.resolve('foo')

let result23 = p21.finally() // 未提供处理程序
let result24 = p21.finally(() => undefined) // 提供了处理程序,但没有显示的返回语句
let result25 = p21.finally(() => {}) // 提供了处理程序,但没有显示的返回语句
let result26 = p21.finally(() => Promise.resolve()) // 提供了处理程序,但没有显示的返回语句
let result27 = p21.finally(() => 'bar') // 提供了处理程序,且有显示的返回值
let result28 = p21.finally(() => Promise.resolve('bar')) // 处理程序返回一个解决的期约
let result29 = p21.finally(() => Error('qux')) // 处理程序返回错误值
setTimeout(console.log, 0, result23) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result24) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result25) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result26) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result27) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result28) // Promise {<fulfilled>: 'foo'},包装父期约的传递
setTimeout(console.log, 0, result29) // Promise {<fulfilled>: 'foo'},包装父期约的传递
  • onFinally处理程序返回待定或拒绝的期约抛出错误,则返回值包装相应的期约(抛出错误包装拒绝的期约)
let result30 = p21.finally(() => new Promise(() => {})) // 处理程序返回一个待定的期约
let result31 = p21.finally(() => Promise.reject()) // 处理程序返回一个拒绝的期约
// Uncaught (in promise) undefined
let result32 = p21.finally(() => {
  throw 'baz' // 处理程序抛出错误
})
// Uncaught (in promise) baz
setTimeout(console.log, 0, result30) // Promise {<pending>},返回相应的期约
setTimeout(console.log, 0, result31) // Promise {<rejected>: undefined},返回相应的期约
setTimeout(console.log, 0, result32) // Promise {<rejected>: 'baz'},返回相应的期约
  • onFinally处理程序返回待定的期约解决后,新期约实例仍后传初始的期约
let p22 = Promise.resolve('foo')
let p23 = p22.finally(
  () => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100)) // 处理程序返回一个待定的期约(100毫秒后解决)
)
setTimeout(console.log, 0, p23) // Promise {<pending>},返回相应的期约
setTimeout(() => setTimeout(console.log, 0, p23), 200) // Promise {<fulfilled>: "foo"},(200毫秒后)待定的期约已解决

非重入期约方法

  • 期约进入落定(解决/拒绝)状态时,与该状态相关的处理程序不会立即执行,处理程序后的同步代码会在其之前先执行,该特性称为非重入
let p24 = Promise.resolve() // 解决的期约,已落定
p24.then(() => console.log('onResolved handler')) // 与期约状态相关的onResolved处理程序
console.log('then() returns') // 处理程序之后的同步代码
/* 
  'then() returns',处理程序之后的同步代码先执行
  'onResolved handler'
*/
  • 即使期约在处理程序之后改变状态(解决/拒绝),处理程序仍表现非重入特性
let synchronousResolve // 全局方法:期约状态状态
let p25 = new Promise((resolve) => {
  synchronousResolve = function () {
    console.log('1: invoking resolve()')
    resolve() // 期约状态改变
    console.log('2: resolve() returns')
  }
})
p25.then(() => console.log('4: then() handler executes')) // 与期约状态相关的onResolved处理程序
synchronousResolve() // 处理程序之后的同步代码:期约状态改变
console.log('3: synchronousResolve() returns') // 处理程序之后的同步代码
/* 
  '1: invoking resolve()'
  '2: resolve() returns'
  '3: synchronousResolve() returns'
  '4: then() handler executes'
*/
  • 非重入特性适用于onResolvedonRejectedcatch()finally()处理程序
let p26 = Promise.resolve()
p26.then(() => console.log('p26.then() onResolved'))
console.log('p26.then() returns')

let p27 = Promise.reject()
p27.then(null, () => console.log('p27.then() onRejected'))
console.log('p27.then() returns')

let p28 = Promise.reject()
p28.catch(() => console.log('p28.catch() onRejected'))
console.log('p28.catch() returns')

let p29 = Promise.resolve()
p26.finally(() => console.log('p29.finally() onFinally'))
console.log('p29.finally() returns')
/* 
  'p26.then() returns'
  'p27.then() returns'
  'p28.catch() returns'
  'p29.finally() returns'
  'p26.then() onResolved'
  'p27.then() onRejected'
  'p28.catch() onRejected'
  'p29.finally() onFinally'
*/

邻近处理程序的执行顺序

  • 若期约添加了多个处理程序,当期约状态变化时,处理程序按添加顺序依次执行

    • then()catch()finally()添加的处理程序均如此
let p30 = Promise.resolve()
let p31 = Promise.reject()

p30.then(() => setTimeout(console.log, 0, 1))
p30.then(() => setTimeout(console.log, 0, 2))

p31.then(null, () => setTimeout(console.log, 0, 3))
p31.then(null, () => setTimeout(console.log, 0, 4))

p31.catch(() => setTimeout(console.log, 0, 5))
p31.catch(() => setTimeout(console.log, 0, 6))

p30.finally(() => setTimeout(console.log, 0, 7))
p30.finally(() => setTimeout(console.log, 0, 8))
/* 
  1
  2
  3
  4
  5
  6
  7
  8
*/

传递解决值和拒绝理由

  • 期约进入落定(解决/拒绝)状态后,会给处理程序提供解决值(兑现)拒绝理由(拒绝)

    • 执行函数中,解决值和拒绝理由分别作为resolve()reject()首个参数,传给onResolvedonRejected处理程序(作为其唯一参数
    • Promise.resolve()Promise.reject()被调用时,接收到的解决值和拒绝理由同样向后传递给处理程序(作为其唯一参数
let p32 = new Promise((resolve, reject) => resolve('foo')) // 执行函数中
p32.then((value) => console.log(value)) // 'foo'
let p33 = new Promise((resolve, reject) => reject('bar')) // 执行函数中
p33.catch((reason) => console.log(reason)) // 'bar'

let p34 = Promise.resolve('foo') // Promise.resolve()中
p34.then((value) => console.log(value)) // 'foo'
let p35 = Promise.reject('bar') // Promise.reject()中
p35.catch((reason) => console.log(reason)) // 'bar'

拒绝期约与拒绝错误处理

  • 在期约的执行函数处理程序中抛出错误会导致拒绝,错误对象成为拒绝理由
let p36 = new Promise((resolve, reject) => reject(Error('foo'))) // 在执行函数中抛出错误
let p37 = new Promise((resolve, reject) => {
  throw Error('foo') // 在执行函数中抛出错误
})
let p38 = Promise.resolve().then(() => {
  throw Error('foo') // 在处理程序中抛出错误
})
let p39 = Promise.reject(Error('foo')) // 在拒绝的期约中抛出错误
setTimeout(console.log, 0, p36) // Promise {<rejected>: Error: foo
setTimeout(console.log, 0, p37) // Promise {<rejected>: Error: foo
setTimeout(console.log, 0, p38) // Promise {<rejected>: Error: foo
setTimeout(console.log, 0, p39) // Promise {<rejected>: Error: foo
  • 可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,错误对象可以让浏览器捕获其中的栈追踪信息
  • 如上述拒绝期约,会在浏览器抛出 4 个未捕获错误:

    • Promise.resolve().then()的错误最后才出现,因为需要在运行时添加处理程序(即未捕获前创建另一个新期约)
/* 上述拒绝期约会抛出4个未捕获错误:栈追踪信息
  Uncaught (in promise) Error: foo
  at <anonymous>:1:51
  at new Promise (<anonymous>)
  at <anonymous>:1:11
  (anonymous)    @    VM1402:1
  (anonymous)    @    VM1402:1

  Uncaught (in promise) Error: foo
  at <anonymous>:3:9
  at new Promise (<anonymous>)
  at <anonymous>:2:11
  (anonymous)    @    VM1402:3
  (anonymous)    @    VM1402:2

  Uncaught (in promise) Error: foo
  at <anonymous>:8:26
  (anonymous)    @    VM1402:8

  Uncaught (in promise) Error: foo
  at <anonymous>:6:9
  (anonymous)    @    VM1402:6
  Promise.then (async)        
  (anonymous)    @    VM1402:5
*/
  • 异步错误的机制与同步是不同的:

    • 同步代码通过throw()关键字抛出错误时,会停止执行后续任何命令
    • 在期约中抛出错误时,不会阻止同步指令,其错误也只能通过异步的onRejected处理程序捕获
throw Error('foo') // 同步代码抛出错误(try/catch中能捕获)
console.log('bar') // 后续任何指令不再执行
// Uncaught Error: foo,浏览器消息队列

Promise.reject(Error('foo')) // 期约中抛出错误(try/catch中捕获不到)
console.log('bar') // 'bar',同步指令继续执行
// Uncaught (in promise) Error: foo,浏览器消息队列

Promise.reject(Error('foo')).catch((e) => {
  console.log(e) // 'Error: foo',在期约中捕获
})
  • 执行函数中的错误,在解决或拒绝期约之前,仍可用try/catch捕获
let p40 = new Promise((resolve, reject) => {
  try {
    throw Error('foo')
  } catch (error) {}
  resolve('bar')
})
setTimeout(console.log, 0, p40) // Promise {<fulfilled>: 'bar'}
  • then()catch()onRejected处理程序在语义上与try/catch相同(捕获错误后将其隔离,不影响正常逻辑),因此onReject处理程序在捕获异步错误后返回一个解决的期约
console.log('begin synchronous execution')
try {
  throw Error('foo') // 抛出同步错误
} catch (error) {
  console.log('caught error', error) // 捕获同步错误
}
console.log('continue synchronous execution')
/*
  'begin synchronous execution'
  'caught error Error: foo'
  'continue synchronous execution'
*/

new Promise((resolve, reject) => {
  console.log('begin synchronous execution')
  reject(Error('bar')) // 抛出异步错误
})
  .catch((e) => {
    console.log('caught error', e) // 捕获异步错误
  })
  .then(() => {
    console.log('continue synchronous execution')
  })
/*

期约连锁与期约合成

  • 多个期约在一起可以构成强大的代码逻辑:期约连锁(拼接)期约合成(组合)

期约连锁

  • 每个期约的实例方法(then()catch()finally())都返回新的期约实例,多个期约可连缀调用形成期约连锁
let p41 = new Promise((resolve, reject) => {
  console.log('first')
  resolve()
})
p41
  .then(() => console.log('second'))
  .then(() => console.log('third'))
  .then(() => console.log('fourth'))
/* 
  'first'
  'second'
  'third'
  'fourth'
*/
  • 若想串行化异步任务,需让每个执行器都返回期约实例
let p42 = new Promise((resolve, reject) => {
  console.log('p42 first')
  setTimeout(resolve, 1000)
})
p42
  .then(
    () =>
      // 执行器返回期约实例
      new Promise((resolve, reject) => {
        console.log('p42 second')
        setTimeout(resolve, 1000)
      })
  )
  .then(
    () =>
      // 执行器返回期约实例
      new Promise((resolve, reject) => {
        console.log('p42 third')
        setTimeout(resolve, 1000)
      })
  )
  .then(
    () =>
      // 执行器返回期约实例
      new Promise((resolve, reject) => {
        console.log('p42 fourth')
        setTimeout(resolve, 1000)
      })
  )
/* 
  'p42 first'(1秒后)
  'p42 second'(2秒后)
  'p42 third'(3秒后)
  'p42 fourth'(4秒后)
*/
  • 可把生成期约的同样的代码封装到一个工厂函数中
function delayedResolve(str) {
  return new Promise((resolve, reject) => {
    console.log(str)
    setTimeout(resolve, 1000)
  })
}
delayedResolve('p42 first')
  .then(() => delayedResolve('p42 second'))
  .then(() => delayedResolve('p42 third'))
  .then(() => delayedResolve('p42 fourth'))
/* 
  'p42 first'(1秒后)
  'p42 second'(2秒后)
  'p42 third'(3秒后)
  'p42 fourth'(4秒后)
*/
  • 期约连锁能有效解决回调地狱问题,上述代码如不用期约的情况如下
function delayedNotPromise(str, callback = null) {
  setTimeout(() => {
    console.log(str)
    callback && callback()
  }, 1000)
}
delayedNotPromise('p42 first', () => {
  delayedNotPromise('p42 second', () => {
    delayedNotPromise('p42 third', () => {
      delayedNotPromise('p42 fourth', () => {})
    })
  })
})
/* 
  'p42 first'(1秒后)
  'p42 second'(2秒后)
  'p42 third'(3秒后)
  'p42 fourth'(4秒后)
*/
  • then()catch()finally()都返回新的期约实例,可任意进行期约连锁
let p43 = new Promise((resolve, reject) => {
  console.log('p43')
  reject()
})
p43
  .catch(() => console.log('p43 catch'))
  .then(() => console.log('p43 then'))
  .finally(() => console.log('p43 finally'))
/* 
  'p43'
  'p43 catch'
  'p43 then'
  'p43 finally'
*/

期约图

  • 一个期约可以有任意多个处理程序,期约连锁可以构建有向非循环图
- A
  - B
    - D
    - E
  - C
    - F
    - G
let A = new Promise((resolve, reject) => {
  console.log('A')
  resolve()
})
let B = A.then(() => console.log('B'))
let C = A.then(() => console.log('C'))
B.then(() => console.log('D'))
B.then(() => console.log('E'))
C.then(() => console.log('F'))
C.then(() => console.log('F'))
/* 
  'A'
  'B'
  'C'
  'D'
  'E'
  'F'
*/

Promise.all()和 Promise.race()

  • Promise.all()接收一个可迭代对象(必传),返回一个新期约,其创建的期约会在一组期约全部解决之后再解决

    • 可迭代对象中的元素通过Promise.resolve()转换为期约
    • 空迭代对象等价于Promise.resolve()
Promise.all([Promise.resolve(), Promise.resolve()]) // 接收1组可迭代对象
Promise.all([3, 4]) // 可迭代对象中的元素通过Promise.resolve()转换为期约
Promise.all([]) // 空迭代对象等价于Promise.resolve()
Promise.all() // TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator)),参数必填
  • Promise.all()合成的期约只会在每个包含期约都解决后才解决
let p44 = Promise.all([
  Promise.resolve(),
  new Promise((resolve, reject) => setTimeout(resolve, 1000)),
])
p44.then(() => setTimeout(console.log, 0, 'all() resolved!')) // 'all() resolved!'(1秒后,非0秒,需等包含的期约先解决)
  • 若有 1 个包含的期约待定,则合成待定的期约,拒绝也同理(拒绝优先于待定)
let p45 = Promise.all([new Promise(() => {}), Promise.resolve()]) // 包含的期约有待定的
setTimeout(console.log, 0, p45) // Promise {<pending>},合成待定的期约
let p46 = Promise.all([new Promise(() => {}), Promise.reject()]) // 包含的期约有拒绝的(也有待定的)
// Uncaught (in promise) undefined
setTimeout(console.log, 0, p46) // Promise {<rejected>: undefined},合成拒绝的期约
  • 若包含的所有期约都成功解决,则合成解决的期约,解决值是所有包含期约的解决值的数组(按迭代器顺序)
let p47 = Promise.all([
  Promise.resolve(1),
  Promise.resolve(),
  Promise.resolve(3),
]) // 包含的所有期约都解决
setTimeout(console.log, 0, p47) // Promise {<fulfilled>: [1, undefined, 3]}
  • 若有期约拒绝,第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由(后续理由不再影响合成期约的拒绝理由),但不影响后续期约的拒绝操作
let p48 = Promise.all([
  Promise.reject(3), // 第一个拒绝的期约,拒绝理由为3
  new Promise((resolve, reject) => setTimeout(reject, 1000, 4)), // 第二个拒绝的期约,拒绝理由为4
])
// Uncaught (in promise) 3
setTimeout(console.log, 0, p48) // Promise {<rejected>: 3},第一个拒绝理由作为合成期约的拒绝理由
p48.catch((reason) => setTimeout(console.log, 2000, reason)) // 3,第一个拒绝理由作为合成期约的拒绝理由,但浏览器不会显示未处理的错误(Uncaught (in promise) 3)
  • Promise.race()Promise.all()类似,接收一个可迭代对象(必传),包装集合中最先落定(解决或拒绝)期约解决值或拒绝理由并返回新期约

    • 可迭代对象中的元素通过Promise.resolve()转换为期约
    • 空迭代对象等价于Promise.resolve()
    • 迭代顺序决定落定顺序
    • 第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,但不影响** 后续期约的拒绝操作
Promise.race([Promise.resolve(), Promise.resolve()]) // 接收1组可迭代对象
Promise.race([3, 4]) // 可迭代对象中的元素通过Promise.resolve()转换为期约
Promise.race([]) // 空迭代对象等价于Promise.resolve()
// Promise.all() // TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator)),参数必填

let p49 = Promise.race([
  Promise.resolve(3),
  new Promise((resolve, reject) => setTimeout(reject, 1000)),
])
setTimeout(console.log, 0, p49) // Promise {<fulfilled>: 3},解决先发生,超时后的拒绝被忽略

let p50 = Promise.race([
  Promise.reject(4),
  new Promise((resolve, reject) => setTimeout(resolve, 1000)),
])
// Uncaught (in promise) 4
setTimeout(console.log, 0, p50) // Promise {<rejected>: 4},拒绝先发生,超时后的解决被忽略

let p51 = Promise.race([
  Promise.resolve(1),
  Promise.resolve(),
  Promise.resolve(3),
])
setTimeout(console.log, 0, p51) // Promise {<fulfilled>: 1},迭代顺序决定落定顺序

let p52 = Promise.race([
  Promise.reject(3), // 第一个拒绝的期约,拒绝理由为3
  new Promise((resolve, reject) => setTimeout(reject, 1000, 4)), // 第二个拒绝的期约,拒绝理由为4
])
// Uncaught (in promise) 3
setTimeout(console.log, 0, p52) // Promise {<rejected>: 3},第一个拒绝理由作为合成期约的拒绝理由
p52.catch((reason) => setTimeout(console.log, 2000, reason)) // 3,第一个拒绝理由作为合成期约的拒绝理由,但浏览器不会显示未处理的错误(Uncaught (in promise) 3)

串行期约合成

  • 多个函数可以合成为一个函数
function addTwo(x) {
  return x + 2
}
function addThree(x) {
  return x + 3
}
function addFive(x) {
  return x + 5
}

function addTen(x) {
  return addFive(addThree(addTwo(x)))
}
console.log(addTen(7)) // 17
  • 与函数合成类似,期约可以异步产生值并将其传给处理程序,后续期约可用之前期约的返回值来串联期约
function addTen(x) {
  return Promise.resolve(x).then(addTwo).then(addThree).then(addFive)
}
setTimeout(console.log, 0, addTen(8)) // Promise {<fulfilled>: 18}
addTen(8).then((result) => console.log(result)) // 18
  • 可使用Array.prototype.reduce()简写上述期约串联
function addTen(x) {
  return [addTwo, addThree, addFive].reduce((pre, cur) => {
    return pre.then(cur)
  }, Promise.resolve(x)) // 归并起点值(归并函数第1个参数)为Promise.resolve(x),第2个参数为数组第1项addTwo
}
setTimeout(console.log, 0, addTen(9)) // Promise {<fulfilled>: 19}
addTen(9).then((result) => console.log(result)) // 19
  • 可将其最终封装成一个通用方法
function compose(...fns) {
  return (x) =>
    fns.reduce((pre, cur) => {
      return pre.then(cur)
    }, Promise.resolve(x))
}
addTen = compose(addTwo, addThree, addFive)
addTen(10).then((result) => console.log(result)) // 20

期约扩展

  • 期约有其不足之处,ECMAScript 未涉及的两个特性期约取消进度追踪在很多第三方期约库中已实现

期约取消

  • 可以提供一种临时性的封装,以实现取消期约的功能
const startButton = document.querySelector('#start') // 开始按钮
const cancelButton = document.querySelector('#cancel') // 结束按钮
let cancelBtnHasClickEvent = false // 结束按钮是否已添加点击事件
/* 
  书中案例每次点击“开始”按钮,都会重新实例化CancelToken实例,给cancelToken追加一个点击事件,打印的'delay cancelled'会随之越来越多
  这里追加一个全局变量cancelBtnHasClickEvent,确保只在首次点击“开始”按钮时,给cancelToken只追加一次点击事件
*/

// CancelToken类,包装一个期约,把解决方法暴露给cancelFn参数
class CancelToken {
  constructor(cancelFn) {
    this.promise = new Promise((resolve, reject) => {
      cancelFn(() => {
        setTimeout(console.log, 0, 'delay cancelled') // 取消计时
        resolve() // 期约解决
      })
    })
  }
}

// 点击事件:开始计时、实例化新的CancelToken实例
function cancellabelDelayedResolve(delay) {
  setTimeout(console.log, 0, 'set delay') // 开始计时
  return new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      setTimeout(console.log, 0, 'delay resolve') // 经延时后触发
      resolve()
    }, delay)
    // 实例化新的CancelToken实例
    const cancelToken = new CancelToken((cancelCallback) => {
      cancelBtnHasClickEvent === false &&
        cancelButton.addEventListener('click', cancelCallback) // 结束按钮添加点击事件
      cancelBtnHasClickEvent = true // 结束按钮已添加点击事件
    })
    cancelToken.promise.then(() => clearTimeout(id)) // 触发令牌实例中的期约解决
  })
}

startButton.addEventListener('click', () => cancellabelDelayedResolve(1000)) // 开始按钮添加点击事件

完整文件 →

期约进度通知

  • ES6 不支持监控期约的执行进度,可通过扩展来实现
// 子类TrackablePromise,继承父类Promise
class TrackablePromise extends Promise {
  // 子类构造函数,接收1个参数(executor函数)
  constructor(executor) {
    const notifyHandlers = []
    // super()调用父类构造函数constructor(),传入参数(执行器函数)
    super((resolve, reject) => {
      // 执行executor()函数,参数为传给TrackablePromise子类的参数,返回执行的结果
      return executor(resolve, reject, (status) => {
        console.log(status)
        /* 
          '80% remaining'(约1秒后)
          '60% remaining'(约2秒后)
          'remaining'(约3秒后)
          'remaining'(约4秒后)
        */
        notifyHandlers.map((handler) => {
          return handler(status)
        })
      })
    })
    this.notifyHandlers = notifyHandlers
  }
  // 添加notify方法,接收1个参数(notifyhandler函数)
  notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandler)
    return this
  }
}

// 创建子类实例,传入参数(executor函数)
let p53 = new TrackablePromise((resolve, reject, notify) => {
  function countdown(x) {
    if (x > 0) {
      notify(`${20 * x}% remaining`)
      setTimeout(() => countdown(x - 1), 1000)
    } else {
      resolve()
    }
  }
  countdown(5)
})
console.log(p53) // Promise {<pending>, notifyHandlers: Array(0)},TrackablePromise实例(子类期约)

p53.notify((x) => setTimeout(console.log, 0, 'progress:', x)) // 调用期约实例的notify()方法,传入参数(notifyhandler函数)
p53.then(() => setTimeout(console.log, 0, 'completed')) // 调用期约实例的then()方法,传入参数(onResolved处理程序)
/* 
  'progress: 80% remaining'(约1秒后)
  'progress: 60% remaining'(约2秒后)
  'progress: 40% remaining'(约3秒后)
  'progress: 20% remaining'(约4秒后)
  'completed'(约5秒后)
*/

p53
  .notify((x) => setTimeout(console.log, 0, 'a:', x))
  .notify((x) => setTimeout(console.log, 0, 'b:', x)) // notice()返回期约,连缀调用
p53.then(() => setTimeout(console.log, 0, 'completed'))
/* 
  'a: 80% remaining'(约1秒后)
  'b: 80% remaining'(约1秒后)
  'a: 60% remaining'(约2秒后)
  'b: 60% remaining'(约2秒后)
  'a: 40% remaining'(约3秒后)
  'b: 40% remaining'(约3秒后)
  'a: 20% remaining'(约4秒后)
  'b: 20% remaining'(约4秒后)
  'completed'(约5秒后)
*/

总结 & 问点

  • 什么是 Promise 类型?如何创建?其不同状态分别表示什么?
  • 执行器函数负责的作用是什么?如何推迟其切换状态?如何避免期约卡在待定状态?
  • 如何实例化一个解决的期约?其值是什么?若传入的参数也是期约结果会怎样?
  • 如何实例化一个拒绝的期约?其拒绝理由是什么?若传入的参数也是期约结果会怎样?
  • Promise.prototype.then()、Promise.prototype.catch()、Promise.prototype.finally()的含义分别是什么?分别接收哪些参数?根据参数的不同,其返回值分别有哪些情况?
  • 如何理解期约的“非重入”?其适用于哪些处理程序?
  • 若同一个期约添加了多个处理程序,当其状态变化时处理程序按怎样的顺序执行?如何把期约的解决值和拒绝理由传递给处理程序?
  • 如何传递解决值和拒绝理由?如何在抛出错误时捕获错误对象?为什么拒绝理由最好统一使用错误对象?
  • 写一段代码,分别在【try/catch 和期约】中捕获【同步和异步】的错误,且不影响正常的其他(后续)代码逻辑
  • 写一段代码,完成多个异步任务的串行:① 不用期约,形成“回调地狱” ② 用期约连锁解决这个问题
  • Promise.all()和 Promise.race()的含义是什么?其在不同情况下分别返回怎样的期约?
  • 写一段代码,串联多个期约,再用 reduce()简化其代码,最后将其封装成一个通用方法
  • 写一段代码实现以下功能:页面有 2 个按钮【开始】和【结束】,单击【开始】实例化一个新期约并打印“开始”,1 秒后期约兑现并打印“成功”,单击【结束】取消该期约并打印“结束”

小讴
217 声望16 粉丝