Promise: 给我一个承诺,我还你一个承诺

时间:2022-05-06
本文章向大家介绍Promise: 给我一个承诺,我还你一个承诺,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

处理concurrent programming,除了threading/multi-processing外,各家语言都有自己的绝活:erlang/elixir是actor model,golang/clojure(core.async)是CSP,haskell/clojure是STM,而javascript是event loop/callback。

callback可能是这几种并发模型里面最好懂的一种方式,就像好莱坞经纪人惯常的做法:don’t call me, I’ll call you back。比如打开数据库,打开要访问的表,写入一列新的数据这样一系列IO密集型的操作,如果同步去做,等待的时间要远大于运算的时间,而使用callback异步处理则消除了等待,大大增强了软件的并行性。然而,callback理解起来很直观,写起代码来很费劲,稍微复杂一些的处理,很容易搞成下图这样的pyramid of doom,也就是俗称的callback hell:

当然,你可以通过重构,把严重嵌套,影响阅读的pyramid拆分成若干个小的pyramid,减少眼睛出血(eye-bleeding)的概率,但毕竟治标不治本。于是,在各种版本的第三方javascript类库里,大家都实现了各自的Promise/A+对象,来减少对callback的依赖。

Promise是这样一个对象,对于任意的异步操作,它提供了一组固定的API,来操作这个结果。我们先看一段代码:

我们看到,如果要把一个异步操作封装成Promise,我们需要首先创建一个Promise,并提供一个包含两个参数 resolvereject的函数,在这个函数里调用你的异步方法(这里用setTimeout模拟)。如果异步方法成功,则在其callback里面调用 resolve,提供成功后获得的数据;如果失败,则调用 reject,提供错误数据。这一般是类库提供者(producer)要做的事情。

对于类库调用者(consumer),拿到一个Promise对象,他可以调用 then 方法来获取异步后的数据,也可以调用 catch 来处理错误。Promise提供了如下机制来简化consumer的代码:

  • then 依旧返回一个Promise,这样,代码的撰写由视觉上的横向延伸(callback hell)变成纵向扩展(chained operation),可读性增强
  • error propagation,在若干个Promise间不断chain的过程中,期间发生的任何error都会被一路传递到最后的Promise的 reject,方便程序员用一个 catch 捕获一条链上的错误,同样的,可读性大大增强

我们看之前那个callback-hell使用Promise撰写后的代码:

代码清晰了不少。下面是Promise处理的状态机:

在ES5,Promise并非原生支持,但有很多第三方的类库支持;在ES6中,Promise形成了一个标准,并且在语言层面原生支持。

Promise在实际使用中除了解决callback hell,让代码可读性增强外,还可以做很多事情。因为Promise实际上可以被视作一个Monad,所以你可以将其用在很多本来难以做composition的场合。比如你有一个处理,需要依赖多个数据源,他们或同步(数据已经在内存中直接可读),或异步(数据需要从数据库或者文件系统读取,甚至来自第三方API),正常来说似乎很难被抽象成一个数据结构。然而,你可以将这些数据源统统封装成Promise(同步的数据可以被视作一个状态立即走到resolved的Promise),这样,可以统一处理。比如说 Promise.all(iterable)(resolve所有结果,返回新的Promise),或者 Promise.race(iterable)(只要有一个结果resolve出来,就立即返回新的Promise,典型的anycast使用场景)。

目前nodejs的库函数还是callback方式,虽说手工转换成Promise非常简单,但毕竟不那么方便。在nodejs app里,你可以使用bluebird(或者es6-promisify)来批量转化nodejs的标准库。比如:

可以被转化成如下的代码:

最后,说说Promise的缺点。

第一,一个Promise只能resolve单个数据,对应于同步处理里的单值数据;如果要处理异步场景下的 "array",那么,Observable是更好的方式。

第二,Promise的API设计感觉有些缺陷,并非lazy(可能是历史原因,也可能考虑到API友好程度),一旦启动,不可终止。如果你需要处理可终止的异步操作,那么,也需要使用Observable。下面是Promise和Observable的代码的对比,可以看到,一旦创建,Promise会立刻执行对象体内的代码(不管你有没有调用 then),而Observable直到subscriber真正要读取时(forEach)才会求值,而且,Observable提供了cancel的API:

即便Oberservable已经开始运行,只要还未完成,调用者都有机会种植它。

OK,今天就先讲到这里,以后我们再讲Observable。