JavaScript进阶之路——认识和使用Promise,重构你的Js代码

  一转眼,这2015年上半年就过去了,差很少一个月没有写博客了,"罪过罪过"啊~~。进入了七月份,也就意味着咱们上半年苦逼的单身生活结束了,今后刻起,咱们要打起十二分的精神,开始下半年的单身生活。你们一块儿加油~~html

  一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深刻人心。在设计API的时候,不论是浏览器厂商仍是SDK开发商亦或是各类类库的做者,基本上都已经遵循着callback的套路。近几年随着JavaScript开发模式的逐渐成熟,CommonJS规范顺势而生,其中就包括提出了Promise规范,Promise彻底改变了js异步编程的写法,让异步编程变得十分的易于理解。今天咱们就来了解一下Promise~~前端

 

 一 、初识Promise

一、什么是promise?html5

  Promise可能你们都不陌生,由于Promise规范已经出来好一段时间了,同时Promise也已经归入了ES6,并且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现现在流行的类Promise类库相比少些API。git

  所谓Promise,字面上能够理解为“承诺”,就是说A调用B,B返回一个“承诺”给A,而后A就能够在写计划的时候这么写:当B返回结果给个人时候,A执行方案S1,反之若是B由于什么缘由没有给到A想要的结果,那么A执行应急方案S2,这样一来,全部的潜在风险都在A的可控范围以内了。es6

Promise规范以下:github

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
  • promise必须实现then方法(能够说,then就是promise的核心),并且then必须返回一个promise,同一个promise的then能够调用屡次,而且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另外一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then能够接受另外一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。

2.promise原理分析ajax

  能够看到promise的规范并非不少,下面咱们一边分析promise一边本身写一个promise的实现。Promise实现的大体思路以下:chrome

  构造函数Promise接受一个函数resolver,能够理解为传入一个异步任务,resolver接受两个参数,一个是成功时的回调,一个是失败时的回调,这两参数和经过then传入的参数是对等的。编程

其次是then的实现,因为Promise要求then必须返回一个promise,因此在then调用的时候会新生成一个promise,挂在当前promise的_next上,同一个promise屡次调用都只会返回以前生成的_next。json

因为then方法接受的两个参数都是可选的,并且类型也没限制,能够是函数,也能够是一个具体的值,还能够是另外一个promise。下面是then的具体实现:

Promise.prototype.then = function(resolve, reject) {  
    var next = this._next || (this._next = Promise());  
    var status = this.status;  
    var x;  
  
    if('pending' === status) {  
        isFn(resolve) && this._resolves.push(resolve);  
        isFn(reject) && this._rejects.push(reject);  
        return next;  
    }  
  
    if('resolved' === status) {  
        if(!isFn(resolve)) {  
            next.resolve(resolve);  
        } else {  
            try {  
                x = resolve(this.value);  
                resolveX(next, x);  
            } catch(e) {  
                this.reject(e);  
            }  
        }  
        return next;  
    }  
  
    if('rejected' === status) {  
        if(!isFn(reject)) {  
            next.reject(reject);  
        } else {  
            try {  
                x = reject(this.reason);  
                resolveX(next, x);  
            } catch(e) {  
                this.reject(e);  
            }  
        }  
        return next;  
    }  
};  

这里,then作了简化,其余promise类库的实现比这个要复杂得多,同时功能也更多,好比还有第三个参数——notify,表示promise当前的进度,这在设计文件上传等时颇有用。对then的各类参数的处理是最复杂的部分,有兴趣的同窗能够参看其余类Promise库的实现。

在then的基础上,应该还须要至少两个方法,分别是完成promise的状态从pending到resolved或rejected的转换,同时执行相应的回调队列,即resolve()reject()方法。

到此,一个简单的promise就设计完成了,下面简单实现下两个promise化的函数:

function sleep(ms) {  
    return function(v) {  
        var p = Promise();  
  
        setTimeout(function() {  
            p.resolve(v);  
        });  
  
        return p;  
    };  
};  
  
function getImg(url) {  
    var p = Promise();  
    var img = new Image();  
  
    img.onload = function() {  
        p.resolve(this);  
    };  
  
    img.onerror = function(err) {  
        p.reject(err);  
    };  
  
    img.url = url;  
  
    return p;  
};  

因为Promise构造函数接受一个异步任务做为参数,因此getImg还能够这样调用:

function getImg(url) {  
    return Promise(function(resolve, reject) {  
        var img = new Image();  
  
        img.onload = function() {  
            resolve(this);  
        };  
  
        img.onerror = function(err) {  
            reject(err);  
        };  
  
        img.url = url;  
    });  
};  

接下来(见证奇迹的时刻),假设有一个BT的需求要这么实现:异步获取一个json配置,解析json数据拿到里边的图片,而后按顺序队列加载图片,每张图片加载时给个loading效果,

 

function addImg(img) {  
    $('#list').find('> li:last-child').html('').append(img);  
};  
  
function prepend() {  
    $('<li>')  
        .html('loading...')  
        .appendTo($('#list'));  
};  
  
function run() {  
    $('#done').hide();  
    getData('map.json')  
        .then(function(data) {  
            $('h4').html(data.name);  
  
            return data.list.reduce(function(promise, item) {  
                return promise  
                    .then(prepend)  
                    .then(sleep(1000))  
                    .then(function() {  
                        return getImg(item.url);  
                    })  
                    .then(addImg);  
            }, Promise.resolve());  
        })  
        .then(sleep(300))  
        .then(function() {  
            $('#done').show();  
        });  
};  
  
$('#run').on('click', run)

这里的sleep只是为了看效果加的,可猛击查看demo

在这里,Promise.resolve(v)静态方法只是简单返回一个以v为确定结果的promise,v可不传入,也能够是一个函数或者是一个包含then方法的对象或函数(即thenable)。

相似的静态方法还有Promise.cast(promise),生成一个以promise为确定结果的promise;Promise.reject(reason),生成一个以reason为否认结果的promise。

咱们实际的使用场景可能很复杂,每每须要多个异步的任务穿插执行,并行或者串行同在。这时候,能够对Promise进行各类扩展,好比实现Promise.all(),接受promises队列并等待他们完成再继续,再好比Promise.any(),promises队列中有任何一个处于完成态时即触发下一步操做。

 

3.标准的Promise

  可参考html5rocks的这篇文章JavaScript Promises,目前高级浏览器如Chrome、Firefox都已经内置了Promise对象,提供更多的操做接口,好比Promise.all(),支持传入一个promises数组,当全部promises都完成时执行then,还有就是更加友好强大的异常捕获,应对平常的异步编程,应该足够了。

  现今流行的各大js库,几乎都不一样程度的实现了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出来的大都是Deferred对象,固然还有angularJs中的$q.这里以jQuery为例,说一下:

// animate  
$('.box')  
    .animate({'opacity': 0}, 1000)  
    .promise()  
    .then(function() {  
        console.log('done');  
    });  
  
// ajax  
$.ajax(options).then(success, fail);  
$.ajax(options).done(success).fail(fail);  
  
// ajax queue  
$.when($.ajax(options1), $.ajax(options2))  
    .then(function() {  
        console.log('all done.');  
    }, function() {  
        console.error('There something wrong.');  
    });  

 

 二 、用Promise组织你的JavaScript代码

  上面咱们了解了Promise,相信你们对Promise有了必定的认识。下面咱们开始动手来写代码,经过几个简单的例子,来加深理解。这里咱们使用浏览器自带的Promise,首先咱们要先检测一些浏览器是否支持Promise,其实很简单,若是是谷歌浏览器,按下F12,打开控制台,如图:

这里咱们能够看到Promise的type是function,也就是说谷歌浏览器是支持promise的。以此为原理,咱们能够写一段JavaScript代码来检测,代码以下:

if(typeof(Promise) === "function"){
    alert("支持Promise");
}
else{
    alert("不支持Promise");
}

通过检测,发现IE11居然不支持promise.建议你们用谷歌浏览器来进行测试吧。

咱们首先来写一个等待的方法,以下:

function wait(duration){
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}

测试这个方法的代码以下:wait(5000).then(function(){alert('hello')}),这段代码很简单,就是等待5秒之后执行一个回调,弹出一个消息。固然,你还能够这样写:

wait(5000).then(function(){alert('hello')}).then(function(){console.log('world')})

怎么样?很简单吧~~

下面来看一些我从网上收集的一些经常使用的JavaScript的promise的写法:

function get(uri){
    return http(uri, 'GET', null);
}

function post(uri,data){
    if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) {
        var params = [];
        for(var p in data) {
            if(data[p] instanceof Array) {
                for(var i = 0; i < data[p].length; i++) {
                    params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i]));
                }
            } else {
                params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
            }
        }
        data = params.join('&');
    }


    return http(uri, 'POST', data || null, {
        "Content-type":"application/x-www-form-urlencoded"
    });
}

function http(uri,method,data,headers){
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(method,uri,true);
        if(headers) {
            for(var p in headers) {
                xhr.setRequestHeader(p, headers[p]);
            }
        }
        xhr.addEventListener('readystatechange',function(e){
            if(xhr.readyState === 4) {
                if(String(xhr.status).match(/^2\d\d$/)) {
                    resolve(xhr.responseText);
                } else {
                    reject(xhr);
                }
            }
        });
        xhr.send(data);
    })
}

function wait(duration){
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}

function waitFor(element,event,useCapture){
    return new Promise(function(resolve, reject) {
        element.addEventListener(event,function listener(event){
            resolve(event)
            this.removeEventListener(event, listener, useCapture);
        },useCapture)
    })
}

function loadImage(src) {
    return new Promise(function(resolve, reject) {
        var image = new Image;
        image.addEventListener('load',function listener() {
            resolve(image);
            this.removeEventListener('load', listener, useCapture);
        });
        image.src = src;
        image.addEventListener('error',reject);
    })
}

function runScript(src) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.src = src;
        script.addEventListener('load',resolve);
        script.addEventListener('error',reject);
        (document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script);
    })
}

function domReady() {
    return new Promise(function(resolve, reject) {
        if(document.readyState === 'complete') {
            resolve();
        } else {
            document.addEventListener('DOMContentLoaded',resolve);
        }
    })
}

 看到了吧,Promise风格API跟回调风格的API不一样,它的参数跟同步的API是一致的,可是它的返回值是个Promise对象,要想获得真正的结果,须要在then的回调里面拿到。

 

 3、用Promise组织JavaScript异步代码

   在比较复杂的页面中,咱们会使用到大量的异步操做。咱们来看看使用Promise会带来怎样的便利吧~~

一、多个异步调用,同步/并行

   例如咱们页面调用了好几个异步函数,咱们要等待全部的异步函数执行完成后,作一些操做,如弹出一个消息框提示用户操做成功。下面咱们拿一个例子来讲明一下:

Promise.all跟then的配合,能够视为调用部分参数为Promise提供的函数。譬如,咱们如今有一个接受三个参数的函数:

function print(a, b, c) {
    console.log(a + b + c);
}

如今咱们调用print函数,其中a和b是须要异步获取的:

var c = 10;

print(geta(), getb(), 10); //这是同步的写法

Promise.all([geta(), getb(), 10]).then(print); //这是 primise 的异步写法

若是用callback的话,咱们就只能一个一个调用了,调用完了geta,而后在其回调函数里面调用getb,最后在getb的回调函数中调用print方法。串行和并行哪一个更快,你们很清楚吧~~

 

 2.竞争

   若是说Primise.all是promise对象之间的“与”关系,那么Promise.race就是promise对象之间的“或”关系。好比,我要实现“点击按钮或者5秒钟以后执行”:

var btn = document.getElementsByTagName('button');

Promise.race(wait(5000), waitFor(btn, click)).then(function(){
    console.log('run!')
})

 

3.异常处理

   异常处理一直是回调的难题,而promise提供了很是方便的catch方法:在一次promise调用中,任何的环节发生reject,均可以在最终的catch中捕获到:

Promise.resolve().then(function(){
    return loadImage(img1);
}).then(function(){
    return loadImage(img2);
}).then(function(){
    return loadImage(img3);
}).catch(function(err){
    //错误处理
})

 

4.复杂流程

接下来,咱们来看比较复杂的状况。

promise有一种很是重要的特性:then的参数,理论上应该是一个promise函数,而若是你传递的是普通函数,那么默认会把它当作已经resolve了的promise函数。

这样的特性让咱们很是容易把promise风格的函数跟已有代码结合起来。

为了方便传参数,咱们编写一个currying函数,这是函数式编程里面的基本特性,在这里跟promise很是搭,因此就实现一下:

function currying(){
    var f = arguments[0];
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        args.push.apply(args,arguments);
        return f.apply(this,args);
    }
}

currying会给某个函数"固化"几个参数,而且返回接受剩余参数的函数。好比以前的函数,能够这么玩:

var print2 = currying(print,11);

print2(2, 3); //获得 11 + 2 + 3 的结果,16

var wait1s = currying(wait,1000);

wait1s().then(function(){
    console.log('after 1s!');
})

有了currying,咱们就能够愉快地来玩链式调用了,好比如下代码:

Promise.race([
    domReady().then(currying(wait,5000)), 
    waitFor(btn, click)])
    .then(currying(runScript,'a.js'))
    .then(function(){
        console.log('loaded');
        return Promise.resolve();
    });

 

 四 、总结

   咱们看到,无论Promise实现怎么复杂,可是它的用法却很简单,组织的代码很清晰,今后不用再受callback的折磨了。promise做为一个新的API,它的API自己没有什么特别的功能,可是它背后表明的编程思路是颇有价值的。

最后,Promise是如此的优雅!但Promise也只是解决了回调的深层嵌套的问题,真正简化JavaScript异步编程的仍是Generator,在Node.js端,建议考虑Generator。

 

 五 、参考资料

JavaScript Promise迷你书(中文版)   http://liubin.github.io/promises-book/

JavaScript Promise启示录       http://www.csdn.net/article/2014-05-28/2819979-JavaScript-Promise

用Promise组织程序                 http://www.w3ctech.com/topic/721

 

 做者:雲霏霏

QQ交流群:243633526

 博客地址:http://www.cnblogs.com/yunfeifei/

 声明:本博客原创文字只表明本人工做中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未受权,贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文链接。

若是你们感受个人博文对你们有帮助,请推荐支持一把,给我写做的动力。

相关文章
相关标签/搜索