《javascript高级程序设计》学习笔记|《javascript高级程序设计》学习笔记 | 11.2.期约

关注前端小讴,阅读更多原创技术文章
期约
  • 期约是对尚不存在结果的一个替身,是一种异步程序执行的机制
相关代码 →
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表示尚未开始或正在执行。最初始状态,可以落定为兑现或拒绝状态,兑现后不可逆
    • 兑现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 {: undefined}let p2 = new Promise((resolve, reject) => reject()) setTimeout(console.log, 0, p2) // Promise {: 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 { },打印实例p3时,还不会执行内部回调

  • resolve()reject()无论哪个被调用,状态转换都不可撤销,继续修改状态会静默失败
let p4 = new Promise((resolve, reject) => { resolve() reject() // 静默失败 }) setTimeout(console.log, 0, p4) // Promise {: undefined}

  • 为避免期约卡在待定状态,可添加定时退出功能,设置若干时长后无论如何都拒绝期约的回调
let p5 = new Promise((resolve, reject) => { setTimeout(reject, 10000) // 10秒后调用reject() }) setTimeout(console.log, 0, p5) // Promise { },10秒内,不调用resolve() setTimeout(console.log, 11000, p5) // Promise {: undefined},10秒外,调用reject() // Uncaught (in promise)

Promise.resolve()
  • 调用Promise.resolve()方法,可以实例化一个解决的期约
let p6 = new Promise((resolve, reject) => { resolve() }) console.log(p6) // Promise {: undefined} let p7 = Promise.resolve() console.log(p7) // Promise {: undefined}

  • 传给Promise.resolve()的第一个参数为解决的期约的值
setTimeout(console.log, 0, Promise.resolve()) // Promise {: undefined} setTimeout(console.log, 0, Promise.resolve(3)) // Promise {: 3} setTimeout(console.log, 0, Promise.resolve(4, 5, 6)) // Promise {: 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 { } setTimeout(console.log, 0, Promise.resolve(p9)) // Promise { } setTimeout(console.log, 0, Promise.resolve(Promise.resolve(p9))) // Promise { }

  • 该方法能够包装任何非期约值(包括错误对象),并将其转换为解决的期约,因此可能导致不符合预期的行为
let p10 = Promise.resolve(new Error('foo')) setTimeout(console.log, 0, p10) // Promise {: Error: foo

Promise.reject()
  • Promise.resolve()相似,Promise.reject()可以实例化一个拒绝的期约并抛出一个异步错误
    • 该错误不能通过try/catch捕获,只能通过拒绝处理程序捕获
let p11 = new Promise((resolve, reject) => { reject() }) console.log(p11) // Promise {: undefined} // Uncaught (in promise)let p12 = Promise.reject() console.log(p12) // Promise {: undefined} // Uncaught (in promise)

  • 传给Promise.resolve()的第一个参数为拒绝的期约的理由,该参数也会传给后续的拒绝处理程序
let p13 = Promise.reject(3) setTimeout(console.log, 0, p13) // Promise { 3 } p13.then(null, (err) => setTimeout(console.log, 0, err)) // 3,参数传给后续拒绝处理程序

  • Promise.reject()不是幂等的(与Promise.resolve()不同),如果参数为期约对象,则该期约会成为返回的拒绝理由
setTimeout(console.log, 0, Promise.reject(Promise.resolve())) // Promise {: 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 {: "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 {: 'foo'},包装上一个期约解决后的值let result2 = p16.then(() => undefined) // 处理程序没有显示的返回语句 let result3 = p16.then(() => {}) // 处理程序没有显示的返回语句 let result4 = p16.then(() => Promise.resolve()) // 处理程序没有显示的返回语句 setTimeout(console.log, 0, result2) // Promise {: undefined},包装默认返回值undefined setTimeout(console.log, 0, result3) // Promise {: undefined},包装默认返回值undefined setTimeout(console.log, 0, result4) // Promise {: undefined},包装默认返回值undefinedlet result5 = p16.then(() => 'bar') // 处理程序有显示的返回值 let result6 = p16.then(() => Promise.resolve('bar')) // 处理程序有显示的返回值 setTimeout(console.log, 0, result5) // Promise {: 'bar'},包装这个值 setTimeout(console.log, 0, result6) // Promise {: 'bar'},包装这个值let result7 = p16.then(() => new Promise(() => {})) // 处理程序返回一个待定的期约 let result8 = p16.then(() => Promise.reject('bar')) // 处理程序返回一个拒绝的期约 // Uncaught (in promise) bar setTimeout(console.log, 0, result7) // Promise {},包装返回的期约 setTimeout(console.log, 0, result8) // Promise {: 'bar'},包装返回的期约let result9 = p16.then(() => { throw 'baz' // 处理程序抛出异常 }) // Uncaught (in promise) baz setTimeout(console.log, 0, result9) // Promise {: 'baz'},包装拒绝的期约let result10 = p16.then(() => Error('qux')) // 处理程序返回错误值 setTimeout(console.log, 0, result10) // Promise {: 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 {: '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 {: undefined},包装默认返回值undefined setTimeout(console.log, 0, result13) // Promise {: undefined},包装默认返回值undefined setTimeout(console.log, 0, result14) // Promise {: undefined},包装默认返回值undefinedlet result15 = p17.then(null, () => 'bar') // 处理程序有显示的返回值 let result16 = p17.then(null, () => Promise.resolve('bar')) // 处理程序有显示的返回值 setTimeout(console.log, 0, result15) // Promise {: 'bar'},包装这个值 setTimeout(console.log, 0, result16) // Promise {: '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 {},包装返回的期约 setTimeout(console.log, 0, result18) // Promise {: 'bar'},包装返回的期约let result19 = p17.then(null, () => { throw 'baz' // 处理程序抛出异常 }) // Uncaught (in promise) baz setTimeout(console.log, 0, result19) // Promise {: 'baz'},包装拒绝的期约let result20 = p17.then(null, () => Error('qux')) // 处理程序返回错误值 setTimeout(console.log, 0, result20) // Promise {: 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 {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result24) // Promise {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result25) // Promise {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result26) // Promise {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result27) // Promise {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result28) // Promise {: 'foo'},包装父期约的传递 setTimeout(console.log, 0, result29) // Promise {: '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 {},返回相应的期约 setTimeout(console.log, 0, result31) // Promise {: undefined},返回相应的期约 setTimeout(console.log, 0, result32) // Promise {: '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 {},返回相应的期约 setTimeout(() => setTimeout(console.log, 0, p23), 200) // Promise {: "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 {: Error: foo setTimeout(console.log, 0, p37) // Promise {: Error: foo setTimeout(console.log, 0, p38) // Promise {: Error: foo setTimeout(console.log, 0, p39) // Promise {: Error: foo

  • 可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,错误对象可以让浏览器捕获其中的栈追踪信息
  • 如上述拒绝期约,会在浏览器抛出 4 个未捕获错误:
    • Promise.resolve().then()的错误最后才出现,因为需要在运行时添加处理程序(即未捕获前创建另一个新期约)
/* 上述拒绝期约会抛出4个未捕获错误:栈追踪信息 Uncaught (in promise) Error: foo at :1:51 at new Promise () at :1:11 (anonymous)@VM1402:1 (anonymous)@VM1402:1Uncaught (in promise) Error: foo at :3:9 at new Promise () at :2:11 (anonymous)@VM1402:3 (anonymous)@VM1402:2Uncaught (in promise) Error: foo at :8:26 (anonymous)@VM1402:8Uncaught (in promise) Error: foo at :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 {: '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 {},合成待定的期约 let p46 = Promise.all([new Promise(() => {}), Promise.reject()]) // 包含的期约有拒绝的(也有待定的) // Uncaught (in promise) undefined setTimeout(console.log, 0, p46) // Promise {: undefined},合成拒绝的期约

  • 若包含的所有期约都成功解决,则合成解决的期约,解决值是所有包含期约的解决值的数组(按迭代器顺序)
let p47 = Promise.all([ Promise.resolve(1), Promise.resolve(), Promise.resolve(3), ]) // 包含的所有期约都解决 setTimeout(console.log, 0, p47) // Promise {: [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 {: 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 {: 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 {: 4},拒绝先发生,超时后的解决被忽略let p51 = Promise.race([ Promise.resolve(1), Promise.resolve(), Promise.resolve(3), ]) setTimeout(console.log, 0, p51) // Promise {: 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 {: 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 {: 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 {: 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 {【《javascript高级程序设计》学习笔记|《javascript高级程序设计》学习笔记 | 11.2.期约】, 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 秒后期约兑现并打印“成功”,单击【结束】取消该期约并打印“结束”

    推荐阅读