Tag Archives: promise

手工实现Js的Promise API

原文地址:http://nius.me/implementing-js-promise-api/

用node开发一阵子的同学,很容易就陷入了callback hell,一层层的嵌套回调,代码实在没法看。于是很多异步转同步工具库出现了。Javascript的harmony版本(ES 6)提供了异步转同步的标准工具:Promise。相最近仔细研读了html5rocks的这篇文章Javascript Promises。里面有很多Promise的例子。不过行文有点快,得慢慢理解。这里为很多还不是很明白Promise的魔法到底是怎样实现的同学,提供一个一步步设计、实现Promise API的讨论。

普通的回调

异步代码都是通过回调函数来实现的。一段ajax请求的代码可能如下:

var req = new XMLHttpRequest();
req.open(url);
req.onload = function(data){ /* callback */ };

//in jQuery
$.get(url, function(data){ /* callback */ });

出现一层层的回调的原因,就在与,在外围的回调函数中,会继续编写异步代码,绑定新的回调函数。例如:

req.onload =       //回调函数绑定
  function(data){      //回调函数的方法头
    var next = parseData(data);   //回调函数同步方法体
  
    anotherReq.open(next);        //回调函数异步方法体
    anotherReq.onload = function(nextData){  
         //... maybe more
    }
}

实现目标:then链

明确一下我们的目的,就不需要编写层层嵌套的回调函数。实现成readFileSync这种的API是不合适的,因为这种实现一定会等待异步代码返回结果,大大降低性能。我们要让代码看起来是同步的,所以,应该是这样的方式:

doSomething()
.then(doAnotherThing)
.then(doThirdThing);

即通过某种Executor,帮助我们实现自动嵌套绑定回调,让代码看起来是先后的,执行起来是异步的。

1. 代码解耦合

从上面我们看出,一段回调代码会包括三个部分

  1. 回调函数绑定
  2. 定义回调函数方法头
  3. 定义回调函数同步方法体
  4. 定义回调函数异步方法体,就是里面嵌套的深层次回调

我们要把函数绑定和函数定义分离解耦,这样才能把嵌套代码拆出来。

//basic version
var callback = function(){}
req.onload = callback;

//more flexible version
var callback = function(){}

var bind = funtion(callback){
  req.onload = callback;
};

bind(callback);

basic version是很常用的分离方法,但是这种方式很难想象出下一步我们该怎么做。变成more flexible version,大家一看就懂了。绑定过程也变成了定义的过程,延迟bind函数的调用,就可以在bind函数上做文章。上Promise!

//ATTETION: Pseudo code
var callback = function(){};

var promise = new Promise(function(callback){  //bind函数体
  req.open(); //async
  req.onload = callback;
})
promise.then(callback);    //调用bind函数

这段代码是不是很神奇了!(虽然还是看不出来怎么实现then.then.then)但是至少我们分离了回调函数绑定和定义,并延迟了绑定的发生。把一切都交给了promise对象,让promise对象来决定中间的过程怎么做!

2. 实现then链

我们要解决的问题是嵌套回调函数。就是说,callback内部也会有回调函数。怎么办?当然是,把内部的回调绑定和回调函数定义也分离!

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(callback){
  req.open(); //async
  req.onload = callback;
})

callback = function(){

   var innerPromise = new Promise(function(innerCallback){
     innerReq.open();  //async
     innerReq.onload = innerCallback;
   })
   innerPromise.then(innerCallback);
}

promise.then(callback);

是不是有点像了!我们只要把innerCallback传给内部的innerPromise的then函数就行了!而innerCallback完全可以在其他地方定义!

我们的目标是then链,但是现在,有两个Promise对象,第二个promise在回调函数执行的时候才被定义,和外面的promise不在一个次元啊。也就是说,要实现then链的话,回调函数们都被传给最外围的promise,而内层的innerPromise的then函数都没办法触发。

那就让他们到一个次元去!用callback的返回值把innerPromise传回去,别忘了,callback可是被传给了promise对象呀,所以promise完全可以拿到callback的返回值呀!这样,外围promise就有了内部promise的引用,就可以把第二个、第三个then函数接到的参数,依次告诉内层的promise!

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(callback){
  req.open(); //async
  req.onload = callback;
})

callback = function(){

   var innerPromise = new Promise(function(innerCallback){
     innerReq.open();  //async
     innerReq.onload = innerCallback;
   })
   return innerPromise;  //告诉外部的promise
}

promise.then(callback).then(innerCallback);

3. 魔法所在:then方法

是不是还是有点绕?这里magic的地方在于,promise对传入的callback会进行改造,再传给bind函数,为了更明确一点,我替换一下变量名:

//ATTETION: Pseudo code
var callback, innerCallback;

var promise = new Promise(function(resolve){  //注意,这里的resolve已经不是callback了!
  req.open(); //async
  req.onload = resolve;
})

callback = function(){

   var innerPromise = new Promise(function(innerResolve){ //这里也不是innerCallback了!
     innerReq.open();  //async
     innerReq.onload = innerResolve;
   })
   return innerPromise;  //告诉外部的promise
}

promise.then(callback).then(innerCallback);

是不是有点明白了?then方法对callback进行改造,例如,把callback外包一个resolve函数。来,自己动手实现一个naive版的Promise!

//ATTETION: Pseudo code, but you can run it.
Promise = function(bind){
  this.level = 0;  //
  this.current = 0;
  this.callbacks = [];
  
  this.bind = bind; //保存传入的bind函数
};

Promise.prototype.then = function(callback){

   var self = this;
   
   this.callbacks[this.current++] = callback;
   
   if(!self.isSolved){
     //从callbacks队列的头部取下第一个resolve,进行绑定
     var res = self.getResolve(this.nextCallback());
     self.bind(res);
     self.isSolved = true;
   }

   return self;   //返回自己,形成then链
};

/**
 * 将callback转换成resolve
 * 
 * @param  {Function} callback 
 * @return {Function} resolve           
 */
Promise.prototype.getResolve = function(callback){
    if(!callback) return;
    var self = this;

    return function(){
      console.log('resolve');

      var nextPromise = callback();   //获得返回的promise,注意callback调用没有参数
      if(nextPromise instanceof Promise){
      
        nextPromise.inherit(self);   //传递当前状态给nextPromise
        nextPromise.execute();   //执行下一个promise
      }
   };
};

Promise.prototype.nextCallback = function(){
   return this.callbacks[this.level++];
};

Promise.prototype.inherit = function(promise){
   this.level = promise.level;
   this.current = promise.current;
   this.callbacks = promise.callbacks;
   this.isSolved = promise.isSolved;
};

/**
 * 执行一个promise,从callbacks中获得一个callback,进行绑定
 */
Promise.prototype.execute = function(){
    var res = this.getResolve(this.nextCallback());
    var nextPromise = this.bind(res);
    if(!nextPromise) return;
    nextPromise.inherit(this);
    nextPromise.execute();
};

这里通过一个数组来记录所有通过链式调用传入的callback方法,然后通过inherit方法,把所有状态完完全全的交给nextPromise,从而让nextPromise继续绑定剩余的callback方法。

这个假想实现有一处硬伤,就是仅仅能让异步代码依次运行,但是异步代码之间没有通信!就是innerPromise得不到外围promise执行结束后的数据呀!es6的解决方案是,如果callback方法返回的不是一个promise,而是一个值,那么这个值将作为下一个callback的首个参数(唯一参数)。

真实的实现还要复杂很多,参考polyfill,这是一个能用现有js实现的Promise库。这个库要做到异步函数立即执行。上面的假实现仅仅做到在调用then时执行bind,实际上bind希望马上执行,毕竟有异步代码在里面,修改上面的代码,这个也是可以实现的。

总结

是不是已经想明白啦?Promise实际上充当了一个执行者的角色,动态的执行每一层次的异步函数的绑定,从而让代码可以看上去,一步步来,then and then and then…真是人类智慧的结晶啊~