初识Promise

2015-09-25
8 min read

对于mongoBD的异步控制,在项目实践中使用了async库来控制复杂的异步回调函数处理。很有幸参加i5ting老师的《Node.js最新技术栈之Promise篇》微课堂。老师主要从promise的起源、实现以及实践和展望几点简述使用Promise的心得。有所收获,现在总结如下,其中例子为老师提供。

roadmap


  1. Why

首先回调和异步在nodejs中十分常见,因为nodejs的最大的优点:高并发,适合适合I/O密集型应用,就是通过异步处理实现。但回调函数的嵌套会导致意大利面条式的代码[1]。比如下面这种一个分为4步的任务:每步任务都需要上一步的返回才能继续执行。

    step1(function (value1) {
      // Do something with value1
      step2(value1, function(value2) {
        // Do something with value2
        step3(value2, function(value3) {
          // Do something with value3
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });

promise[2]就是来解决js中这种频繁回调导致的问题,promise是可以通过像jQuery的链式调用这样来控制回调函数,链式写法: $('#tab').eq($(this).index()).show().siblings().hide(); 。每个方法都返回this对象传递给下一个方法。比如下面这个例子:

    var obj = {
      step1:function(){
        console.log('a');
        return this;
      },
      step2:function(){
        console.log('b');    
        return this;
      },
      step3:function(){
        console.log('c');
        return this;
      },
      step4:function(){
        console.log('d');
        return this;
      }
    }

    console.log('——\n');
    obj.step1().step2().step3();
    console.log('——\n');
    obj.step4().step2().step1();

输出结果abc以及dba。这就是promise,而promise的意思是还未获得的值,只是作为一个占位符给下一个方法。正如promise是还未获得的值,deferred表示未尚未完成的任务步骤,而deferred可以被解决(resolved),也可以被拒绝(rejected)。

回到上面的回调函数:step1().step2().step3().step4(),每个操作都是独立的,可组装。但如果需要上一步的输出为下一步的输入,比如linux的 pipe:ps -ef|grep node|awk ‘{print $2}’|xargs kill -9,或者需要捕获异常,控制流程呢?这就是promise规范的起因。

  1. What

官方给出的promise/a+规范的定义为:Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。还是上面那个例子:异步操作的最终结果: return step1().step2().step3().step4()。如果在promise里,就是这样:

    return step1().then(step2).then(step3).then(step4).catch(function(err){
      // do something when err
    });

定义中交互方式的几点都都对应了,step2传入then方法来获取step1的返回值,catch来捕获error,catch后的回调函数就是处理error。有一个reject,还对应一个resolve方法。

  • reject 是拒绝,跳转到catch error
  • resolve 是解决,下一步,即跳转到下一个promise操作

比如下方这个例子,step1失败就不运行step2,调到下一个promise操作,执行step3。

    function step1(){
      if(false){
        return step3().then(step4).catch(function(err){
          // do something when err
        });
      } 
    }
   return step1().then(step2).then(step3).then(step4).catch(function(err){
  // do something when err
});

再来看看promise的几个要点。

  • promise 是一个包含了兼容promise规范then方法的对象或函数。我们可以这样理解,每一个promise只要返回的可以then的都可以。就像上面举例返回的this一样,只要每一个都返回this,她就可以无限的链式下去。
  • thenable 是一个包含了then方法的对象或函数。
  • value 是任何Javascript值。 (包括 undefined, thenable, promise等).
  • exception 是由throw表达式抛出来的值。当流程出现异常的适合,把异常抛出来,由catch err处理。
  • reason 是一个用于描述Promise被拒绝原因的值。

最后总结一下promise,1) 每个操作都返回一样的promise对象,保证链式操作;2) 每个链式都通过then方法;3) 每个操作内部允许犯错,出了错误,统一由catch error处理;4) 操作内部,也可以是一个操作链,通过reject或resolve再生成流程。还有一些复杂的规范细节[3]需要在实践中才知道意义。

nodejs里的promise实现主要有一下几种,最近在使用async来控制回调流程。

  • bluebird 拥有不错的性能,后面继续讲
  • q Angularjs的$q对象是q的精简版
  • then teambition作品
  • when Promises/A+和when()的实现
  • async 最简单的)
  • eventproxy 朴灵作品,使用event来

其他语言实现,详见 https://promisesaplus.com/implementations

  1. how

下面进入到如何实现promise的内容,一个很初步的原型如下代码:

    //promise原型对象
    var Promise = function () {
    };
    //类型判断
    var isPromise = function (value) {
      return value instanceof Promise;
    };
    //给defer对象返回resolve和promise
    var defer = function () {
      var pending = [], value;
      //声明
      var promise = new Promise();
      //增加then方法    
      promise.then = function (callback) {
        if (pending) {
          pending.push(callback);
        } else {
          callback(value);
        }
      };
      //返回
      //value传参
      return {
        resolve: function (_value) {
          if (pending) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
              var callback = pending[i];
              callback(value);
            }
            pending = undefined;
          }
        },
        promise: promise
      };
    };

我们还可以看看q,angularjs的$q就是它的精简版,它把q的7个版本是如何实现的都详细记录4了。

了解到基本的实现就需要考虑在项目中的实践了。i5ting的经验是公司项目使用bluebird,而小项目使用async。几种实现的benchmark。这样一看bludbird确实性能比较好。比如并行执行任务的比较如下:

bluebird特性

  • 速度最快
  • api和文档完善,(对各个库支持都不错)
  • 支持generator等未来发展趋势
  • github活跃
  • 还有让人眼前一亮的功能点

下面举例的是bluebird的promisify,promisify原理就是你给他传一个对象或者prototype,它去遍历,给他们加上async方法,此方法返回promise对象,你就可以像上面那样使用promise的特点了,但这样得谨防对象过大,导致内存问题。熟悉fs的API的都知道,fs有fs.readFile()fs.readFileSync()方法,但没有fs.readFileAsync()。实际上fs.readFileAsync()是bluebird加上去的,使用promisifyAll()来实现这个功能。

    var fs = Promise.promisifyAll(require(“fs”));

按照MVC的流程,首先定义模型:

    var mongoose = require(‘mongoose’);
    var Schema = mongoose.Schema;
    var Promise = require(“bluebird”);
    UserSchema = new Schema({
      username: String,
      password: String,
      created_at: {
        type: Date,
        "default": Date.now
      }
    });
    var User = mongoose.model(‘User’, UserSchema);
    Promise.promisifyAll(User);
    Promise.promisifyAll(User.prototype);

然后就是业务逻辑,直接调用findAsync的:

    User.findAsync({username: ‘username’}).then(function(data) {
      …
    }).catch(function(err) {
      …
    });

可以优化的地方在于,直接mongoose的static和method上扩展,不暴露太多细节。面对promise,保证每个操作都是函数,使得流程可以组装。比如下列这种情况,Team可以添加新用户:

    //用函数封装操作
    function find_user(){
      return User.findByAsync(user_id);
    }
    //直接调用mongoose方法
    Team.updateByIdAync(a, b, c).then(find_user).catch(error);

扩展一下数据库driver这边,ioredis也支持的很好,能将与redis的交互建立在promise的控制下,而mongoose支持promise也越发证明这个工具与原生driver的不同。

  1. es6的实现 generator/yield

generator是es6新添加的功能,generator(生成器函数):不会立即执行,需要再执行迭代操作(.next),yield抛出断点等待next的调用。使用这种方法的库有co,其使用例子如下:

    co(function* (){  
      yield Something.save();
    }).then(function() {
      // success
    })
    .catch(function(err) {
      //error handling
    });

而yield的并行直接则是这样写: yield[fun1(), fun2()];

  1. es7的实现 async/await es7则通过async关键词来执行异步操作,使用await执行异步操作的例子如下,可以看到这与async库之间的区别:

     async function save(fun1){
       try{
         await fun1.save();
       }
     }
    

最后再总结一次promise的要点:

  • 异步操作的最终结果,尽可能每一个异步操作都是独立操作单元
  • 与Promise最主要的交互方法是通过将函数传入它的then方法(thenable)
  • 捕获异常catch error
  • 根据reject和resolve重塑流程

而generator是一种新的定义方式,定义操作单元,尤其在迭代器的情况,搭配yield来执行,可读性上差了很多,好处是真的解耦了。co是一个中间产品,可以说是给generator增加了promise实现,可读性和易用性是愿意好于generator + yield的。最后我们看看async,它实际上是通过async这个关键词,定义的函数就可以返回promise对象,可以说async就是能返回promise对象的generator,yield关键词以及被generator绑架了,那它就换个名字,叫await。

其实从这段历史来看,反复就是promise上的折腾,只是加了generator这个别名,只是async是能返回promise的generator。

这次学习大致就是这么多内容,其细节还需要在项目上实践,也确定了使用bb作为以后express的异步控制库。


参考:

  1. Spaghetti Code:意大利面条代码的定义
  2. Promise Github:源码
  3. Promise+官网:规范细节
  4. q的7个版本详细记录:设计思路
  5. Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.:promise和deferred这些概念的理解
comments powered by Disqus