11.2 期约
期约是对尚不存在的结果的一个替身。
- 终局 (eventual)
- 期许 (future)
- 延迟 (delay)
- 迟付 (deferred)
11.2.1 Promises/A+规范
- 早期 期约机制在jQuery和Dojo中以 Deferred API 的形式出现
- 2010 CommonJS项目实现了 Promises/A
- 2012 Promiese/A+ 组织fork了CommonJS的 Promises/A 建议
- 最终 ECMAScript6增加了 Promise 类型
11.2.2 期约基础
Promise作为引用类型时可以通过new操作符实例化,
实例化时需要传入 执行器(executor) 函数作为参数。
如果不提供executor参数会报错。
1 | let p = new Promise(() => {}); |
1. 期约状态机
- 待定 (pending) 表示尚未开始或正在执行
- 兑现/解决 (fulfilled/resolved) 表示已经成功完成
- 拒绝 (rejected) 表示没有成功完成
初始状态
期约处于 待定(pending) 状态中。
待定状态
期约可以 落定(settled) 为2种状态:
- 兑现(fulfilled) 状态代表成功
- 拒绝(rejected) 状态代表失败
这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象。
期约故意将异步行为封装起来,从而隔离外部的同步代码。
2. 解决值、拒绝理由及期约用例
- 抽象表示一个异步操作是否完成
- 期约封装异步操作会实际生成某个状态改变后需要访问的值
为了支持这两个用途,期约提供了两个私有的内部属性(可选,默认值为undefined):
- value(值): 期约状态切换为兑现时提供
- reason(理由): 期约状态切换为拒绝时提供
3. 通过执行函数控制期约状态
- 初始化期约的异步行为
- 控制状态的最终转换
控制期约转换的2个函数参数通常这样命名:
- resolve() 将状态切换为fulfilled
- reject() 将状态切换为rejected
executor函数是Promise的初始化程序。
下面这段程序并非异步操作,
因为在初始化Promise时,executor已经改变了Promise的状态。
1 | let p1 = new Promise((resolve, reject) => resolve()); |
使用setTimeout推迟切换状态,可以看到Promise仍处在pending状态中:
1 | let p = new Promise((resolve, reject) => setTimeout(resolve, 1000)); |
Promise状态的转换不可撤销,修改状态操作静默失败:
1 | let p = new Promise((resolve, reject) => { |
为了防止长时间卡在pending状态,
可以添加一个定时退出的功能:
1 | let p = new Promise((resolve, reject) => { |
4. Promise.resolve()
功能
调用 Promise.resolve() 静态方法可以实例化一个 resolved Promise ,
实际上它可以 把任何值都转换为一个Promise对象 。
参数
Promise.resolve() 的第一个参数表示解决的期约的返回值 value 。
多余的参数会被忽略。
1 | // Promise<resolved>: undefined |
幂等性
传入参数如果本身就是Promise对象,就相当于空包装。
1 | let p = Promise.resolve(7); |
这种幂等性还会保留传入期约的状态。
1 | let p = new Promise(() => { }); |
5. Promise.reject()
功能
实例化一个 rejective Promise 并抛出一个异步错误,
异步错误 不能通过try/catch捕捉,只能通过 拒绝程序 捕获。
参数
Promise.reject() 的第一个参数作为 拒绝期约的理由,
该理由会顺次传给后续的拒绝程序。
1 | let p = Promise.reject(3); |
非幂等性
传给 Promise.reject 一个Promise对象作为参数,
该对象会作为拒绝期约的理由。
1 | //Promise {<rejected>: Promise} |
6. 同步/异步执行的二元性
1 | try { |
1 | try { |
Promise是 同步对象 ,也是 异步执行模式 的媒介。
- 同步模式: 错误直接抛到执行同步代码的线程中
- 异步模式: 错误通过浏览器异步消息队列来处理
11.2.3 期约的实例方法
1. 实现Thenable接口
答:在对象上实现 then 方法。
1 | class MyThenable{ |
ECMAScript暴露的 异步结构中的任何对象 都有一个then方法。
2. Promise.prototype.then()
then方法接收2个可选的参数:
- param1 onResolved处理程序(如果选择不传,需要使用null/undefined占位)
- param2 onRejected处理程序
1 | function onResolved(id) { |
如果参数非函数类型,会被 静默忽略。
如果只想提供onRejected参数,需要对onResolved参数进行 占位。
1 | function onResolved(id) { |
Promise.prototype.then返回值:一个新的期约实例。
1 | let p1 = new Promise(() => { }); |
对于不同返回值的几种情况:
onResolved函数提供了返回值
会将返回值通过Promise.resolve()包装成新的期约。
1 | let p6 = p1.then(() => 'bar'); |
没有提供onResolved函数
会包装上一个期约解决之后的值。
1 | let p1 = Promise.resolve('foo'); |
onResolved函数没有显式返回语句
会包装默认返回值 undefined。
1 | let p3 = p1.then(() => undefined); |
抛出异常
会返回 rejective Promise。
1 | // Uncaught (in promise) baz |
返回错误值
不会触发reject,会将错误对象包装在一个 resolved Promise 中。
1 | let p11 = p1.then(() => Error('qux')); |
onRejected处理程序返回值也会被 Promise.resolve() 包装,
虽然有点诡异,但是 onRejected 本身的任务就是 捕获异步错误,
也就是 捕获错误后不抛出异常 。
onRejected返回值案例
1 | let p1 = Promise.reject('foo'); |
3. Promise.prototype.catch()
catch用于 给期约添加拒绝处理程序。
唯一参数是 onRejected处理程序。
相当于 Promise.prototype.then(null, onRejected) 的语法糖。
1 | let p1 = new Promise(() => { }); |
4. Promise.prototype.finally()
finally()方法用于 给期约添加onFinally处理程序。
无论Promise转化为 fulfilled 还是 rejected ,
onFinally都会执行,
用这个方法能够避免 onResolved 和 onRejected 程序中出现冗余代码。
onFinally无法获知Promise的状态,所以这个方法主要用于添加清理代码。
1 | let p1 = Promise.resolve(); |
finally()与catch()、then()的区别
主要就是onFinally被设计为一个 状态无关的方法,
重点在于 父期约的传递 上。
1 | let p1 = Promise.resolve('foo'); |
特殊情况下的finally返回值
当出现下列情况时,会返回相应的期约:
- 期约待定:Promise {<pending>}
- onFinally程序抛出了错误
- onFinally程序显式抛出或返回了一个 rejective Promise
1 | let p1 = Promise.resolve('foo'); |
5. 非重入期约的方法
当期约进入 fulfilled 状态时,
相关异步处理程序不会立即执行,仅仅会被 排期 ,
其后的同步代码一定会在它之前执行。
1 | // 创建解决的期约 |
先添加处理程序后解决期约。
1 | let synchronousResolve; |
非重入适用范围。
- onResolved/onRejected
- catch
- finally
1 | let p1 = Promise.resolve(); |
6. 邻近处理程序的执行顺序
1 | let p1 = Promise.resolve(); |
7. 传递解决值和拒绝理由
期约会提供value/reason给相关状态的处理程序。
1 | let p1 = new Promise((resolve, reject) => resolve('foo')); |
8. 拒绝期约与拒绝错误处理
1 | let p1 = new Promise((resolve, reject) => reject(Error('foo'))); |
异步错误的副作用
正常情况下抛出的错误会阻塞后续指令的执行:
1 | // Uncaught Error: foo |
期约中抛出的错误不会阻塞后续同步指令的执行:
1 | // Error: foo |
对比同步错误与异步错误处理
同步错误处理
1 | // 1 |
异步错误处理
1 | // 1 |
11.2.4 期约连锁与期约合成
一个期约接一个期约的拼接。
将多个期约组合为一个期约。
1. 期约连锁
通过连缀方法调用就可以构成所谓的 期约连锁:
1 | let p = new Promise((resolve, reject) => { |
串行化异步任务
每个执行器都返回一个期约实例,
每个后续期约都等待之前的期约。
1 | let p1 = new Promise((resolve, reject) => { |
将生成期约代码提取成工厂函数
1 | function delayedResolve(str) { |
使用回调函数重写连锁期约(回调地狱)
1 | function delayedExecute(str, callback = null) { |
串联期约的相关方法
1 | let p = new Promise((resolve, reject) => { |
2. 期约图
期约连锁的结构类似于 有向非循环图:
- 一对多关系: 一个期约可以有任意多个处理程序
- 节点: 组成连锁期约的每一个期约
- 有向顶点: 使用实例添加的处理程序
- 层序遍历: 期约处理程序是按照添加顺序执行的
1 | // A |
3. Promise.all()和Promise.race()
- 功能: 创建一个期约,该期约会在一组期约全部解决后再解决
- 存在一个待定期约,合成期约也待定
- 存在一个拒绝期约,合成期约也拒绝
- 参数: 可迭代对象
- 返回值: 一个新期约
- 合成期约解决值: 所有包含期约解决值的数组,按照迭代器顺序
- 合成期约拒绝理由: 即第一个拒绝的期约的理由
Promise.all()参数
1 | let p1 = Promise.all([ |
合成期约在每个期约都解决后才解决
1 | let p = Promise.all([ |
合成期约的pendinng和rejected条件
1 | let p1 = Promise.all([new Promise(() => { })]); |
合成期约的解决值
1 | let p = Promise.all([ |
合成期约会静默处理所有包含期约的拒绝操作
1 | let p = Promise.all([ |
- 功能: 返回一个包装期约,该期约是一组集合中最先解决/拒绝的期约的镜像
- 不会对解决或拒绝的期约区别对待
- 会静默处理所有包含期约的拒绝操作
- 参数: 可迭代对象
- 返回值: 一个新期约
Promise.race()参数
1 | let p1 = Promise.race([ |
Promise.race()返回值
1 | // 先解决 |
Promise.race()对拒绝操作的静默处理
1 | let p = Promise.race([ |
4. 串行期约合成
异步产生值并将其传给处理程序。
函数合成
1 | function addTwo(x) { return x + 2; } |
使用期约重现函数合成
1 | function addTwo(x) { return x + 2; } |
使用Array.prototype.reduce()再简化
1 | function addTwo(x) { return x + 2; } |
提炼通用合成函数
1 | function addTwo(x) { return x + 2; } |
11.2.5 期约扩展
一个主要原因就是这样会导致 期约连锁 和 期约合成 过度复杂化。
下面涉及到两个第三方期约库中具备但是ECMAScript未涉及的特性。
一般针对于期约正在处理过程中,程序不再需要结果的情形。
因为期约的逻辑只要开始执行,就无法阻止其执行到完成。
提供一种临时性的封装。
生成的令牌实例提供一个接口,可以用于取消期约。
同时也提供一个期约实例,用来触发取消后操作并求值取消状态。
1 | class CancelToken { |
取消令牌案例
尽管本案例的思路是在触发开始按钮的时候,
对取消按钮进行事件绑定。
但是由于没有手动进行事件解绑,
因此导致取消按钮事件会不断堆叠。
解决方法:
在添加事件监听器时将option.once配置为true,表示仅触发一次事件。
1 | cancelButton.addEventListener('click',cancelCallback,{once:true}); |
1 | <button id="start">Start</button> |
1 | class CancelToken { |
简化版取消期约
相比取消令牌方法更加简单粗暴,
即是将开始按钮触发事件包装成异步操作,
给取消按钮添加提前resolve事件,
在期约进入resolve状态时对取消按钮进行解绑。
相较官方案例扩展度更低,但简单粗暴。
1 | function cancellableDelayedResolve(delay) { |
async/await实现取消期约
1 | let startButton = document.querySelector('#start'); |
扩展Promise类,添加 notify() 方法:
1 | class TrackablePromise extends Promise { |