JS Promise Note
一直对 Promise 这个概念感到迷迷糊糊,实在是受不了了,决定系统的过一次相关的知识点。
以下笔记主要基于著名的 JavaScript 迷你书(中文版),感谢原作者 @azu 和翻译者 @liubin!
本文将不定期陆续更新。
API
Constructor
|
|
Instance Method
|
|
Static Method
Promise.all()
, Promise.resolve()
, Promise.reject()
Status
Promise 有三种状态:has-resolution/Fulfilled
, has-rejection/Rejected
, unresolved/Pending
用法
Promise.resolve()
|
|
同理,Promise.reject()
会第一时间返回一个 promise 对象(如果有调用 .catch(onReject)
则会在下一 tick 中执行 onReject
使用 Promise 确保异步流程
|
|
Promise Chain
.then
和 .catch
方法可以进行链式调用,比如:
|
|
在这个链式调用中,taskA 和 taskB 可以通过两种方式使执行流程经过 onRejected 函数:
- 抛出一个异常
- 返回一个
rejected
状态的 Promise 对象(推荐使用这个方法)
使用第二种方法的理由:
- 更加直观,因为
.catch
方法本来的含义就是在 Promise 对象状态变为rejected
时执行的回调。 - 避免
throw
关键字造成的副作用(影响 debug 等)
在 then
中注册的回调函数可以通过 return
返回一个值,这个返回值会传给后续的 then
或者 catch
的回调函数
但 then
的结果总是一个新创建的 promise 对象。如果 then
中注册的回调函数的返回值就是一个 Promise 对象,则 then
的结果就是这个对象。
所以我们可以在 then
中返回一个带 reject
状态的 Promise 对象:
|
|
Anti-pattern: 对同一个对象同时调用 then 方法
显然这样的处理是不能得到预想中的结果的,必须修改成使用 Promise Chain 的方式。
|
|
处理 IE8 下 catch
保留字的问题
在ECMAScript 3中保留字是不能作为对象的属性名使用的。而IE8及以下版本都是基于ECMAScript 3实现的,因此不能将 catch 作为属性来使用,也就不能编写类似 promise.catch() 的代码,因此就出现了 identifier not found 这种语法错误了。
解决方案: 使用 then
而不使用 catch
;如果一定要使用 catch
的话:
|
|
Promise.all()
Promise.all 接收 Promise 对象组成的数组作为参数,两个 promise 对象的初始化会同时进行,当所有的 promise 对象的状态转变为 fulfilled
或者 rejected
之后才会处理 Promise Chain 上的 then 函数,且得到的执行结果的顺序与原 promise 数组的顺序一致。
|
|
Promise.race()
race()
方法和 all()
类似,接收一个 promise 对象数组为参数,但区别在于:
Promise.race 只要有一个 Promise 对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。
但 race 胜出的 Promise 对象不会阻止其他 Promise 对象的执行。
.catch()
最好的理解方法就是将 catch 方法当做是 promise.then(undefined, onRejected)
,它们在本质上没有区别。
Test Promise
- 使用 Mocha 对 Promise 进行测试
- 在
it()
中直接返回 Promise 对象,则不需要使用done
- 测试 Promise 对象时,应该覆盖 Promise 对象的两种状态(Fulfilled, Rejected),同时检查两种状态时的返回值。满足这样条件的测试叫做可控测试(controllable tests)
定义一个叫做 shouldRejected
的函数,用于测试期待返回状态为 onRejected
的 Promise:
|
|
同理可以写一个 shouldFulfilled
的 helper…
Advanced
一些实现了 Promise 的第三方类库:
Polyfills
Promise Extensions
Thenable
- Thenable 就是一个具有
.then()
方法的一个对象。 - 通过
Promise.resolve()
可以将一个 Thenable 对象转换为一个标准的 Promise 对象 - 很多第三方库提供了将 Thenable 对象转换为其实现的 Promise 对象的途径。所以在内部使用 Thenable,便于在不同的 Promise 类库之间进行相互转换。
实现一个 Thenable 对象非常简单:
|
|
Deferred & Promise
Deferred 和 Promise 的关系
- Deferred 拥有 Promise
- Deferred 具备对 Promise 状态进行操作的特权方法
使用 Promise 实现 Deferred
|
|
这样写的好处有:
- 减少一层缩进
- 不需要一开始就将处理流程写成一大段代码,只需要先创建 deferred 对象,在任何时机调用
resolve
,reject
方法。
如果说Promise是用来对值进行抽象的话,Deferred则是对处理还没有结束的状态或操作进行抽象化的对象,我们也可以从这一层的区别来理解一下这两者之间的差异。
换句话说,Promise代表了一个对象,这个对象的状态现在还不确定,但是未来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而Deferred对象表示了一个处理还没有结束的这种事实,在它的处理结束的时候,可以通过Promise来取得处理结果。
实现超时机制: Promise.race()
以下这段代码实现了一个简单的超时函数,当目标 Promise 中的任务在超过 ms 后未执行完(状态未变更),则由和其竞争的 timeout
promise 抛出一个异常,从而调起后续 Promise Chain 中的 catch 方法。
|
|
不过这里还有一个问题,就是如果业务 promise 在执行过程中出现了问题,抛出一个错误(或者调用 reject),那么在 Promise Chain 后续的 catch 函数中,其实我们无法分辨到底是系统超时了还是业务 promise 出现了问题。当然,检查 error.msg
具体的字符串值是可以勉强做到的,但这样的实现非常的不美观。
一种理想的方案是自定义一个 TimeoutError
类型的对象,通过检查 error instanceof TimeoutError
来判断捕获到的错误是否为一个超时错误。
使用 ES6 规范里面的 class
, extend
自然是轻轻松松,但也无妨看下在 ES5 下的实现方案。
插播:创建一个继承 Error 的类 TimeoutError
|
|
另一种思路,来源于 CoffeeScript 的实现:
|
|
在实现了简单的超时之后,我们希望能够在 XHR 超时后取消其请求操作(以免阻塞后面可能的 XHR 请求),需要用到 xhr.abort()
方法。
对上一节中实现的 getURL
函数稍加改进,改成返回一个带 promise 和 abort 方法的对象,配合 timeoutPromise
方法就可以完成整个业务逻辑。
Promise.prototype.done
如果你使用过其他的 Promise 实现类库的话,可能见过用 done 代替 then 的例子。
这些类库都提供了 Promise.prototype.done 方法,使用起来也和 then 一样,但是这个方法并不会返回 Promise 对象。
虽然 ES6 Promises 和 Promises/A+ 等在设计上并没有对 Promise.prototype.done 做出任何规定,但是很多实现类库都提供了该方法的实现。
done
不返回 Promise 对象done
发生的异常会直接抛到外面
在开发中,如果忘记编写 catch 函数处理 Promise Chain 中运行时错误,那么这些错误会被 “内部消化”,而不会被外部所得知,这样就给 debug 造成了巨大的困难。使用 done 的意义在于避免这样的情况。
在 setTimeout 中抛出一个异常并不会被捕获!!!
|
|
Promise & method chain
TODO
Promise & sequence
TODO
Reference
Promise
- Promise Objects - ECMAScript Language Specification
- Writing Promise - Using Specifications // W3C
- JavaScript Promises - Thinking Sync in an Async World // Speaker Deck
- JavaScript Promise // Google Web Developer
- You’re Missing the Point of Promises
- es6-promise: A polyfill for ES6-style Promises
- Promise Anti-patterns
- Promises/A+
- You’re Missing the Point of Promises
- JavaScript 异步编程的 Promise 模式