Promises/A+ 学习记录
Promise 解决的问题, Promises/A+ 规范的参考来源, Promise 的简单实现, 常见 Promise 实现的差异, Promise 的不足, Promise 的反模式,
此文为资料收集和自己的学习感悟,所以也没啥干货,别期待太高哦。
大纲
- Promise 解决的问题
- Promises/A+ 规范的参考来源,如Monad,Deferred
- Promise 的简单实现
- 常见 Promise 实现的差异
- Promise 的不足,哪些实现补足了相关不足
- Promise 反模式
Promise 解决的问题
我们先看看Promise要解决什么问题,在诞生之前又是怎么解决的[1]。
JS是单线程执行的,这意味着两段脚本不会同时执行,必须一个接一个执行,那么怎么让他们“并行”执行呢?在浏览器中,JS也会与其他任务共享一个线程,通常情况下是JS、绘制、更新样式和处理用户操作处在同一线程中,其中一个任务会延迟其他任务。
通常我们的做法是通过事件和回调来解决该问题。
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
在这段代码执行完毕后,JS就可以停止执行了,然后等待某个事件发生,侦听器被执行。
然而有时候这些事件在我们开始侦听之前就可能已经发生了,因此我们还需要complete
之类的属性来解决该问题,然而这样也解决不了发生错误时的侦听,DOM也没给出解决方案。
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
然而事件也不是最佳方案,上面是不用关心前后执行流程的场景,如果是比较复杂的异步流程呢?理想情况下是这么编排的
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
使用Promise标准化方式来写就是这样
img1.ready().then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
// all loaded
}, function() {
// one or more failed
});
promise 在这看上去和事件侦听很像,但是还是有些差别
- promise 只能成功或失败一次,不能多次,也不能从失败转为成功
- 如果 promise 已成功或失败,那么后续添加的回调也会正确执行,即便事件已经事先发生
在回调之外,还有消息订阅
Promises/A+ 规范
从 Promise 身上我们可以看到熟悉的影子,譬如 Monad、Deferred、Future 等等[2]。
规范里面有几个术语[3][4]
- promise,是带有then方法的对象或者函数,then表现符合规范描述
- thenable,是带有then方法的对象或者函数
- value,是任何Javascript值。 (包括 undefined, thenable, promise等)
- exception,是由throw表达式抛出来的值
- reason,是一个用于描述Promise被拒绝原因的值
然后一个promise又有几种状态
- pending,可以转换到fulfilled或rejected状态
- fulfilled,不能转换成任何其它状态,必须有一个 value
- rejected,不能转换成任何其它状态,必须有一个 reason
在浏览器正式实现此规范之前,Promise 就被第三方库广为实现,譬如Q,when,RSVP.js,bluebird,其中 bluebird 是号称比原生还快的第三方 Promise 库。
尽管 Promise 实现遵照标准化行为,但其整体 API 有所不同,值得一提的是jQuery的Promise类型,只是 Deferred 的子集,与 Promises/A+ 不兼容;原生 JS 实现的 API 更接近RSVP.js。早期 Promise 在 DOM 中的实现叫做 Futures,后来改名为 Promises,最后移入到 JS 中。
这里简单补充下链式调用、函子、Monad 等概念。
链式调用大家都经常使用,譬如 array.filter(isOdd).map()
,这里会引入一个函数式概念:函子。
首先,链式调用是个广泛的概念:函数的返回值是一个对象时,直接调用返回值中对象的方法也是链式调用。准确的术语叫作方法链,是一种常见的 OOP 句法。链式调用有这么几个好处
- 让调用过程更接近自然语言
- 让原本多个参数的复杂方法化作多个简单参数的方法
只要你看一眼 f(1)(2)(3)(4)
和 num(1).add(2).sub(3).mul(4)
就能体会上述好处了。
只要返回的对象上也有可以调用的方法就能形成链式调用,其中有个特殊情况:函子,函子 是实现了 map 并遵守一些特定规则的容器类型 。这两个高亮的新概念就不讲了,不然深究下去就太多了,这是来源于 FP 的概念。通俗的讲,就是 对象的主要方法(map)都返回同一类型的新对象 ,而函子中又有另一个特例:Monad。 我们使用Promise 的时候相信大家都有一个发现:你可以返回一个常规值,也可以返回一个 promise,这就是 Monad 的特性:自动解除嵌套(auto flatten the functors)。这也是使我们摆脱回调地域和嵌套地域的特性。
Promise 的简单实现[5]
// TODO
常见 Promise 实现的差异
刚好有张表,就直接借来用了,成表时间为2014/03/03
Promises/A+ | Progression | Delayed promise | Parallel synchronization | Web Workers | Cancellation | Generators | Wrap jQuery | |
---|---|---|---|---|---|---|---|---|
Bluebird | ✓ | ✓ (+389 B) | ✓ (+615 B) | ✓ (+272 B) | – | ✓ (+396 B) | ✓ (+276 B) | ✓ |
ES6 Promise polyfill | ✓ | – | – | ✓ | – | – | – | – |
jQuery | – | ✓ | – | ✓ | – | – | – | ✓ |
Q | ✓ | ✓ | ✓ | ✓ | – | – | ✓ | ✓ |
RSVP | ✓ | – | – | ✓ | – | – | ✓ | – |
when | ✓ | ✓ | ✓ | ✓ | – | ✓ | ✓ | ✓ |
Promise 的不足
Promise 和 Generator 搭配使用能写出更干净的代码[6],但是有理论门槛:迭代器模式。通常大家更倾向于使用 async/await
语法糖。不过我建议都学好,技多不压身。
对于标准的 Promise 有以下几个不足[7]
- Eagerness 模式,Lazyness 不可选,导致了副作用不易处理
- 无法取消,导致资源占用超过必要时长
- 专用 API,主要是 then 方法导致的重名冲突,让很多实现需要对 Promise 进行了特殊处理。
- 混淆了 exceptions 和 expected failures
- 允许用户不处理异常
- 如果错误处理没及时处理,你的进程可能处于无法恢复的无效状态
综上:每一个你创建的 promise,都存在因为一个 expected failure 而崩掉你整个进程的风险。好在上述的大多数问题都可以通过严格的编码规范而解决:一定要有错误处理,并且及时绑上错误处理,不能 reject 会被误认为异常的值,不能直接或者间接地创建带有 then 方法的对象。
在 Promise 之外,还有另一个相似的概念:Observable。
Single | Multiple | |
---|---|---|
Synchronous | Variable | Array |
Asynchronous | Promise | Observable |
我们需要寻找的库它应该具备以下特性
- Lazyness,以使能够自我处理副作用
- 可取消,以便资源在计算链中及时释放
- 不做任何的类型特殊处理
- 不应该混淆 exceptions 和 expected failures
- 强制要求错误处理
在其他语言中早已存在了这样的功能:Futures。不妨看看 Fluture、bluebird 或者 RxJS。
Promise 反模式
- 显式构建
.then(success, fail)
当已经存在 promise 或者 thenable 时显式构建是一种反模式,因为这样可能会导致内部的错误丢失而不被调用者接收到;正确的用法是直接 return
已有 promise,这样错误就能被最终的错误处理函数接收到。
从 doThat(function(err, success))
转变到 doThat().then(success, err)
让我们避免了紧耦合的过程函数, 而 then
方法主要是流程性操作,没有 onRejected 处理的必要,放到最后的 .catch
就行;另外,使用 exception 也是一个提前释放资源的 hacky 技巧。