Promises/A+ 学习记录

Promise 解决的问题, Promises/A+ 规范的参考来源, Promise 的简单实现, 常见 Promise 实现的差异, Promise 的不足, Promise 的反模式,

此文为资料收集和自己的学习感悟,所以也没啥干货,别期待太高哦。

大纲

  1. Promise 解决的问题
  2. Promises/A+ 规范的参考来源,如Monad,Deferred
  3. Promise 的简单实现
  4. 常见 Promise 实现的差异
  5. Promise 的不足,哪些实现补足了相关不足
  6. 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 在这看上去和事件侦听很像,但是还是有些差别

  1. promise 只能成功或失败一次,不能多次,也不能从失败转为成功
  2. 如果 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 就被第三方库广为实现,譬如QwhenRSVP.jsbluebird,其中 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]

  1. Eagerness 模式,Lazyness 不可选,导致了副作用不易处理
  2. 无法取消,导致资源占用超过必要时长
  3. 专用 API,主要是 then 方法导致的重名冲突,让很多实现需要对 Promise 进行了特殊处理。
  4. 混淆了 exceptions 和 expected failures
  5. 允许用户不处理异常
  6. 如果错误处理没及时处理,你的进程可能处于无法恢复的无效状态

综上:每一个你创建的 promise,都存在因为一个 expected failure 而崩掉你整个进程的风险。好在上述的大多数问题都可以通过严格的编码规范而解决:一定要有错误处理,并且及时绑上错误处理,不能 reject 会被误认为异常的值,不能直接或者间接地创建带有 then 方法的对象。

在 Promise 之外,还有另一个相似的概念:Observable。

Single Multiple
Synchronous Variable Array
Asynchronous Promise Observable

我们需要寻找的库它应该具备以下特性

  1. Lazyness,以使能够自我处理副作用
  2. 可取消,以便资源在计算链中及时释放
  3. 不做任何的类型特殊处理
  4. 不应该混淆 exceptions 和 expected failures
  5. 强制要求错误处理

在其他语言中早已存在了这样的功能:Futures。不妨看看 Fluturebluebird 或者 RxJS

Promise 反模式

  1. 显式构建
  2. .then(success, fail)

当已经存在 promise 或者 thenable 时显式构建是一种反模式,因为这样可能会导致内部的错误丢失而不被调用者接收到;正确的用法是直接 return 已有 promise,这样错误就能被最终的错误处理函数接收到。

doThat(function(err, success)) 转变到 doThat().then(success, err) 让我们避免了紧耦合的过程函数, 而 then 方法主要是流程性操作,没有 onRejected 处理的必要,放到最后的 .catch 就行;另外,使用 exception 也是一个提前释放资源的 hacky 技巧。


  1. 使用Promises--GoogleDev ↩︎

  2. FuturesAndPromises ↩︎

  3. SegmentFault-Promise/A+规范 ↩︎

  4. Promises/A+ ↩︎

  5. PromiseImplementation ↩︎

  6. GeneratorsPattern ↩︎

  7. BrokenPromises ↩︎