处理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,并提供一个包含两个参数 resolve
,reject
的函数,在这个函数里调用你的异步方法(这里用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。