Tag Archives: javascript

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

Backbone笔记

  • listenTo的callback会接受到一个参数,就是other的当前对象。
object.listenTo(other, event, callback)
  • Model的状态属性都在model.attributes对象中,通过defaults可以来赋初值定义。之后通过get和set获得
var M = Backbone.Model.extend({</p> 
    defaults:function(){
    
    return {key: value };
    
    })
    
    new M.get('key');
    
    new M.set('key', 'hello world')

 

  • 数组的构造函数,默认接受参数(models, options),就是说,初值必须是models这个值。
  • model默认都会有id,如果你的后台不是id,可以通过指定model.idAttribute = ‘_id’来修改,于是Bakbone就会把id的值设为_id这个属性的值。
  • 数组的add方法会进行去重,粗略的说,包含有相同id的model对象都不会添加进去,实际代码用get方法判断,get如下:
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];

也就是说,如果定义model的时候,把 id的初值设置为null,由于cid是自动产生的client id,不会重复,因此,Collection.add会把这个新的对象加入到数组中。这里,cid的产生方法是_.uniqueId(‘c’),就是说产生一个c打头的唯一id,目测不会与后台的id编号冲突。

小试backbone

收到@liuda101的强烈推荐,最终还是看了一下backbone。也由于之前shunote的代码基本上是靠jquery堆出来的,完全用knockout重写,不太现实。看完果然发现,backbone确实什么也没做,但是帮你梳理了代码的思路,让代码变的更加清晰了。

View

Backbone的View的意思是一段显示的内容,即html通过模板构造,然后对其绑定事件。这对于SPA来说,还是很重要的。之前确实觉得绑定事件这种代码没地方放,放到业务逻辑模块感觉不对,单独放又与业务逻辑分不开,因为事件是Js运转的起点。下面是从官方demo todo删减出来的一段代码。

  Var AppView = Backbone.View.extend({
      el: $('#todoapp'),
      events: {
        "click #clear-completed": "clearCompleted",
        "keypress #new-todo": "createOnEnter"
      },
      initialize: function(){
        this.listenTo(Todos, "add", this.addOne);
        this.listenTo(Todos, "all", this.render); //所有发生的事件都会触发
      },
      render: function(){
        // 重新渲染视图
        this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
      },
      addOne: function(){
        var view = new TodoView({model: todo});
        this.$("#todo-list").append(view.render().el);
      },
    })

 

很清晰的一段代码。el代表这个View的最外围的元素,这里就是说页面上已经有这个元素了。如果没有需要稍稍换一种方式声明。events就是绑定元素内其他元素的事件。initialize是初始化,在View实例化的时候可以调用,在这里面Listen to 数据Model的变化,

注意,数据Model变化也是“事件”,所以是Listen to,但是不是界面事件,而是数据变化事件。View需要和数据变化保持一致,所以View要listen to models

那么,数据是怎么变化的呢?当然还是界面事件导致的(input)。因此,在View的events里定义的事件,就用来listen to 界面变化,这里的事件处理函数,会改变models。

View做了什么?

也就是说,View做了两件事,

  1. 当用操作界面时,改变model ——view是攻
  2. 当model改变时,更新界面。 ——view是受

是不是很晕?那就对了,因为MVC在这里其实不太好用。这里如果要梳理的更清楚,应该使用MVVM模型,参考Understanding MVVM – A Guide For JavaScript Developers

  • ViewModel: 界面数据模型(界面事件产生时,更新ViewModel,它对界面上展示的数据建模)
  • View 需要被model同步的界面(上面的受,knockout的data-bind),同时把界面事件events通知给ViewModel(攻ViewModel)
  • Model 真正的数据模型,ViewModel相当于封装了Model,把需要展示的部分暴露出来,Model藏在ViewModel的后面。

剩下的部分

继续Backbone,

Model和Collection了,模型和模型数组,没啥好说的,大家都差不多。

Router,这个是特色,帮你管理URL。具体的没看,不过写过SPA的应该都猜的出来,不同的url做不同的事嘛,构造和解析嘛。

Sync,帮助你将数据持久化,就是ORM的部分,搞定后台,REST接口。

Backbone带来的一系列小福利。

哦了。

review前端代码的一些心得

  1. js的函数声明和实现写在一起,不方便review。写代码时应该在格式上注意。seajs模块对外暴露的接口是一个不错的放声明的地方
  2. 模块内部函数,应该直接用function来定义,不要绑定到特定对象上,方便review。这些function纯粹是功能性的、面向过程的,仅仅用来内部查看,不需要绑定到对象上,也不需要暴露出去。

// 后面接着更新。。。

Viewport & media query in design for mobile

Responsive design is undoubtly the trend to design a website for both big screen and mobile screen. The magic lies on the media query in CSS.

What media query do?

It query how the screen or device that presenting the web page looks like. For example, how long is the device width, a 1024px screen or a 640px one.

Like this (in html file):

<link rel="stylesheet" media="screen and (color)" href="example.css" />

or like this (in css file):

@media all and (min-width:500px) { /* your css here */ }

So, you can change your css responsively and respectively as the screen changes.

What viewport do?

Another relative technique is the viewport. It is created by Safari and can hardly be a standard. But it is there to be used. When a mobile device render a traditional web page in a little screen, to avoid using a narrow width to make the page looks weird (for those has never been developed to fit mobile screens), the browser use a larger screen, say, 980px, to render the page. Then the browser zooms out to fit the content to the 320px screen. If you don’t want your site be zoomed out, you can use this:

<meta name="viewport" content="width=device-width">

Then, the web page is rendered as a device-width browser, it will look like a narrowed desktop browser by dragging.

No picture means nothing (No pic you say a jb)

fixed, device-width viewport
fixed, default viewport
flexible, device-width viewport
flexible, default viewport

So what?

With device-width viewport, at least your font-size would be as normal and clear to read. If you use flexible or fixed design in your website only, you should not modify the viewport, for that users would zoom in & out to read your content, while your layout wouldn’t go weird.

Anything to do with media query?

Of course! If you use media query to use defferent css in defferent screen-size, you would absolutely get the best and optimized effect of your sites. But that would be quite a lot of work.

For more reference, check out here:

Media Queries, W3C Recommendation 19 June 2012

Using the viewport meta tag to control layout on mobile browsers

坑爹的viewport

折腾seajs

新项目用seajs来搭建
之前用过老版本,也已经忘记的差不多了,打算全面使用新版本

刚好顺便用coffee和less的插件。

于是华丽丽的悲剧了。插件就是用不起来,按照 seajs常用插件的介绍写了seajs.config,前端各种报找不到的错= =b

开始是没有安装seajs插件,在@lepture的细心指导下,总算明白是要在assets目录下

  • spm install gallery.less
  • spm install gallery.coffee

可以安装的插件列表在:aralejs,果然是在用绳命在发布插件啊!

安装完了还是跑不起来,浏览器说GET seajs/plugin-less.js失败,这个文件在哪= =!?

最终发现,seajs的dist里面是包含了这些常用插件的激活代码的,但是用spm安装的时候,不会包含进来!

于是从github上一个个把插件弄下来,放到seajs-modules/seajs/1.3.0/里面去,
总算可以跑了!内牛啊!!!

感谢@lepture的耐心指导!

Decode escaped HTML in JavaScript

My co-worker escapes all the data sent to the backend using a Java tool. The characters are converted into something like &#12345; which means its original unicode in decimal. Meanwhile, html character entities are used when escaping so that the character like “<” will be &lt; .

Frontend should not only decode the string transmitted back but also convert the string into JSON objects. lazily I choose the tricky way to unescape the string, like this:

function getJson(data){
    return $.parseJSON($('

').html(data).text());
}

But I find this code sometimes doesn’t work and it seems the the data has been cut off in some place which misleading me thinking some sort of memory leaking.

I tried and tried, and with the help of JSON Lint, I found that the problem laid on the character .

In the approach above, the data is converted into characters which means the &quot has been converted into , but the json string returned use in its format! So, it is the that cut off the string!

Then, I wrote something like this:
(Thanks to Convert HTML Character Entities back to regular text using javascript)

function u2text(text){
    var $p = $('

');
    return text.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z]+);?/gi, function($0, $1) {
        if ($1[0] === "#") {
            return String.fromCharCode($1[1].toLowerCase() === "x" ? parseInt($1.substr(2), 16) : parseInt($1.substr(1), 10));
        } else {
            return $p.html($0).text().replace(/"/g, "\\u0022");
        }
    });
}

In this code the Chinese character in unicode will be converted using the String.fromCharCode, while the html character entities will be decoded using the <p> block scheme, and obviously, quotes are treated differently. Be cautious that the quote character should be converted into “\u0022″. If you use “\u0022″, you change nothing.

After all, the decoding is dangerous for the escaping text has been decoded. In my situation the escaping stuff will be handed over to front-end template system, say Dust.js, which will take very good care of the security problems.

State Pattern in JavaScript

In recent coding I encounter a situation that in different contexts, the button’s action should change accordingly. In other words, the page has several states, and in different states, the action of a same button should be different. I know in Java we have State Pattern in hand to deal with this situation, which take the advantage of Java’s OO features like inheritance and polymorphism. I don’t want to use JavaScript’s class which would definitely raise the complexity of the codes. With the help of the realization of Strategy Pattern in Book JavaScript Patterns, I write something like this.

Say, we have two buttons in the page, which are add and del. SPAGE is the namespace.

var SPAGE = {
    btn: {},  //collection of all btn's click event handlers
          //it has different states
              //in this example there are two buttons: add, del

    states:{ //state set of btn
        full: {}, //state 1: full
        half: {}  //state 2: half
    },

    setState: function(s){ //switch state
                  this.btn = this.states[s];
              }
}

SPAGE.states.full = (function(){ //realize event handlers of state 'full'
    var add = function(){
        print('full add click');
    }
    var del = function(){
        print('full delete click');
    }
    return {
        add: add,
        del: del
    }

})();
SPAGE.states.half = (function(){ //realize the methods of state 'half'
    var add = function(){
        print('half add click');
    }
    var del = function(){
        print('half delete click');
    }
    return {
        add: add,
        del: del
    }
})();

//set state and call
SPAGE.setState('full');
SPAGE.btn.add();    //click the button add
                    //output: full add click
SPAGE.setState('half');
SPAGE.btn.del();    //click the button del
                    //output: half delete click

So, I think I take advantages of JavaScript’s nice features of objects. I don’t know how you guys solve the similar situation. Any discussion is welcomed warmly~:)