Tag Archives: 同步

手工实现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…真是人类智慧的结晶啊~

Jquery Animation’s Synchronization and Nested Animation Using a Counter

Animation In Jquery is quite simple and easy to use. But things come complicated when you want to create a series of animations. Recently I am building an app which heavily relies on the animation, and get really exhausted sereral times. I learn a lot from this.

Animation Sync

Here is what I write at the very beginning.

$('#left').animate({left: '300px'}, 300, function(){
    $('#left #left-node').attr('id', 'center-node');
    $(this).attr('id', 'center');
    //...
});
$('#center').animate({left: '600px'}, 300, function(){
    $('#center #center-node').attr('id', 'right-node');
    $(this).attr('id', 'right');
    //...
});
$('#right').animate({left: '900px'}, 300, function(){$(this).remove();})

Three blocks will move right at the same time. Then left and center block will change their id to fit the new position. Then here comes the problem: although the three animations in different elements seem to finish at the same time, they finish one after another indeed. And I even don’t know which one! So the operations of changing the id will result in a mess. Needless to say other animation I want to do after.

I need a function that can be called after all these animations finish. I resort to the Queue in the Jquery but find the queue is binding to a particular element. Then a school mate tells me to use a counter and a shared callback to synchronize the animation. That’s quite a good idea. Then the code will be like this.

var sync = 3;
function callback(){
    if(--sync > 0) return;
    //...
}
$('#left').animate({left: '300px'}, 300, callback);
$('#center').animate({left: '600px'}, 300,callback);
$('#right').animate({left: '900px'}, 300, callback);

Nested Animation

As I dynamically generate the html, so I want to make the animation more beautiful. So I write this.

//...generate the html
$("#left-node .node, #center-node .node").hide().fadeIn(300, function(){
    $("#left-node").animate({left: '300px'}, 300);
    $("#center-node").animate({left: '600px'}, 300);
});

I want all the nodes to fade in and after that move the right position. Then the disaster occurred. Since the $() selector has selected more than one element, so the callback will be triggered more than once! Say 15 nodes have been selected, the left-node and the center-node will animate for 15 times! And the default animation queue in both element will function, that is to say, they will animate for 15*300 = 4500 ms. The worst part is, if any event trigger other animation on the two element, the upcoming animation will be held up till the repeated animation finish. The users will see a blocked response in their eyes.
This can also be fixed by the counter mentioned above.

var $nodes = $("#left-node .node, #center-node .node");
var sync = $nodes.size();
$nodes.hide().fadeIn(300, function(){
    if(--sync > 0) return;
    //... further animation
});

OK! All animations go as expected now. But this is really a hard work for a novice of JavaScript like me.

Well, maybe the best way to avoid these complicated cases is not to design so many animations in your web page.