Node学习笔记 ---- 异步编程浅谈(2)

145 阅读4分钟
本文来自互联网引用,原为链接:water7it.com/archives/no…

异步编程中会遇到的问题

开篇依然用我生活中的一件事来建立问题模型,然后看看在用程序语言描述模型时会有什么问题。

前一阵子找工作,一般来说流程可以描述为:发简历->等通知->电话面试->1面->2面->成功,而期间任何一步都可能失败(throw error),catch之后一般会重开一个"副本"继续发简历->等通知->电话面试...

一般情况下,由于当时不会得出面试结果,所以可以认为这是一个异步模型。

此时假设我有两个目标:Co.A和Co.B, 并且A比B优先级高,我并不会同时发简历,那么用伪代码描述我的整个求职历程可能这样的:

Me.request({    target: Co.A,    success: Me.be_interviewed_by_phone({        target: Co.A,        success: Me.first_interview({            target: Co.A,            success: Me.secend_interview({                target: Co.A,                success: '在Co.A成功求职',                error: ... // 转入Co.B流程            })            error: ... // 转入Co.B流程        }),        error: ... // 转入Co.B流程    }),    error: Me.request({        target: Co.B,        success: ..., // 类似Co.A流程        error: '求职失败'    })})

以上是把人的思维最直接的转换为代码时的一种情况,其中使用很多...省略,否则会更长更繁琐。

这里我们不讨论如何封装针对不同公司的求职过程。可以看出就算把...替换成一个函数,这样的代码依然有两个问题:

  1. 嵌套过深,一般这样的代码被称为"Pyramid of Doom"(恶魔金字塔),看上去就很dirty。
  2. 异常处理较为麻烦,由于error几乎是同一个回调,但却要书写N次,以后维护也很麻烦。

在初期编写Node程序时,我经常遇到这样的问题,甚至问题会被放大、延伸,毕竟业务要比上述模型复杂很多。

Promise/Deferred模式

Promise/Deferred模式是解决上述问题的方案之一,在2009年被Kris Zyp抽象为一个提议草案,发布在CommonJS规范中。

我认为简单一句话,Promise可以让你编写出来的程序如人的思维一样:【第一步】-> 【第二步】-> 【第三步】, 并且捕获异常。

如果按照Promise/Deferred模式去实现上述模型,最终的代码可能是这样:

var request = function(co) {    return Me.request(co)             .then(function() {return Me.be_interviewed_by_phone(co)})             .then(function() {return Me.first_interview(co)})             .then(function() {return Me.secend_interview(co)})             .then(function() {console.log('在'+co+'求职成功')});} request(Co.A).error(function(){    return request(Co.B).error(function(){console.log('求职失败')});})

可以看出这是一个【流水账】式的代码,使用request函数封装之后代码量也变得极少。

下面我们详细说说Promise/Deferred模式。

目前,CommonJS草案中已经包括Promise/A、Promise/B、Promise/D这些异步模型。由于Promise/A较为常用也较为简单,我们主要来看看这个。

在API定义上,Promise/A很简单,只需要具备then()方法即可。一般来说,then()的方法定义如下:

then(fulfilledHandler, errorHandler, progressHandler)

通过继承Node的events模块,我们可以实现一个简单Promise模块。

var Promise = function() {    EventEmitter.call(this);}util.inherits(Promise, EventEmitter); // util是node自带的工具类 Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {    if(typeof fulfilledHandler === "function") {        this.once('success', fulfilledHandler);    }    if(typeof errorHandler === "function") {        this.once('error', errorHandler);    }    if(typeof progressHandler === "function") {        this.on('progress', progressHandler);    }    return this;}

以上是Promise部分,可以看到它负责把回调函数与事件绑定,起一个"承诺(Promise)"的作用。为了完成整个流程,还需要触发这些回调函数,实现这些功能的对象通常被称为Deferred,即延迟对象。实现如下:

var Deferred = function() {    this.state = 'unfulfilled';    this.promise = new Promise();} Deferred.prototype.resolve = function(obj) {    this.state = 'fulfilled';    this.promise.emit('success', obj);} Deferred.prototype.reject = function(obj) {    this.state = 'failed';    this.promise.emit('error', obj);} Deferred.prototype.progress = function(obj) {    this.promise.emit('progress', obj);}

这些代码只是最基础的原理展示,病不能用于实际使用。归根结底还是因为Promise是高级接口,内部使用了EventEmitter这一底层接口,而为了让自己的应用程序Promise化,仍需要进一步包装,十分麻烦,所以一般会利用一些更加成熟的Promise实现方案。

Q模块

Q模块是Promise/A规范的一个实现。Q的defer部分有一个叫makeNodeResolver的prototype,实现代码我就不再贴出,大家可以通过npm install q安装以后查看。

顾名思义,makeNodeResolver返回一个Node风格的回调函数。

假设我们要把fs.readFile包装成支持Promise风格的readFile方法,我们可以通过Q模块这样实现:

var readFile = function(file, encode) {    var deferred = Q.defer();    fs.readFile(file, encode, deferred.makeNodeResolver());    return deferred.promise;}

然后就可以很方便的调用啦,比如这样:

readFile('foo.txt', 'utf-8').then(function(data){    // success case}, function(err){    // failed case})