JavaScript Promise

为什么分享这个?


    最近有时间review前端代码的时候,发现大部分代码里不管是各种事件处理回调、ajax回调,就是各种回调函数。回调函数少了还好,一旦多了起来而且必须讲究执行顺序的话,回调函数开始嵌套很多层,对于程序员来讲非线性执行的编程会让只会觉得难以掌控,Promise就是为了把JS复杂的嵌套转换成常思维的线性代码,更易于维护和理解。带来一定的便利。




'''1)什么是promise'''


   Promise就是一个对象,用来传递异步操作的消息。就是说A调用B,B返回一个“承诺”给A,当B返回结果给到的时候,A执行方案1,反之如果B因为什么原因没有给到A想要的结果,那么A执行应急方案2,这样一来,所有的潜在风险都在A的可控范围之内了。


'''2)解决什么问题'''


     传统数据回调多层嵌套问题,如下代码原理是,当执行一些异步操作时,我们需要知道操作是否已经完成,所有当执行完成的时候会返回一个回调函数,表示操作已经完成。


     ajax({
        url: url1,
        success: function(data) {
            ajax({
                url: url2,
                data: data,
                success: function() {
                    
                }
            });
        }
    });


  这种多重回调机制理解起来很简单,但是实际的应用当中会有缺点


  1> 在需要多个操作的时候,会导致多个回调函数嵌套,导致代码不够直观
  2> 如果几个异步操作之间并没有前后顺序之分(例如不需要前一个请求的结果作为后一个请求的参数)时,同样需要等待上一个操作完成再实行下一个操作。


  使用了 Promise 对象之后可以用一种链式调用的方式来组织代码,让代码更加直观。而且由于 Promise.all 这样的方法存在,可以让同时执行多个操作变得简单


    function A() {
        ajax({
            url: url1,
            success: function(data) {
                B(data);
            }
        });
    }
    function B(data) {
        ajax({
            url: url2,
            success: function(data) {
                ......
            }
        });
    }
  new Promise(A).done(B);


'''3)ES6 Promise'''


      2015年6月Es6正式发布,其中Promise被列为正式规范。作为ES6中最重要的特性之一,我们接下来要对其有一个必要的理解.


      '''1> Promise规范'''


         * 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)


         * 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换


         * promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致


         * then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。
       
     
      


       '''2> resolve和reject'''
        Promise的构造函数接收一个函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected,如下代码:


       var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
           console.log('执行完成');
           resolve('成功');
       }, 2000);
      })
     运行代码,会在2秒后输出“执行完成”。此时并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,为何要包在一个函数中。其实就是为了return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。更改如下:


     function run(){
         var p = new Promise(function(resolve, reject){
            //做一些异步操作
            setTimeout(function(){
                console.log('执行完成');
                resolve('成功');
           }, 2000);
        });
        return p;            
     }


     run().then(function(data){
        console.log(data);       
        //......
     });
    
   在run()的返回上直接调用then方法,then接收一个函数,并且会拿到我们在run中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“成功”。此时你是否已经明白?原来then里面的函数就跟我们平时的回调函数一个作用,能够在run这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。这个时候再看是不是有点对Promise这个东西高看了以前?跟以往的多层回调封装是一样的道理。可是并不是如此,我们看下面代码:


   callBack是这样的:


    function run(callback){
      setTimeout(function(){
          console.log('执行完成');
          callback('成功');
      }, 2000);
    }


   run(function(data){
      console.log(data);
   });


运行这段代码结果是一样的,Promise的优势又能体现哪里比起callBack,此时如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数 接着是不是再传入callback2? 再下一步callback3? 多层回调时最能体现Promise的优势,它可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作,如下:




run()
.then(function(data){
    console.log(data);
    return run2();
})
.then(function(data){
    console.log(data);
    return run3();
})
.then(function(data){
    console.log(data);
});






三个回调函数的定义:


function run1(){
    var p = new Promise(function(resolve, reject){        
        setTimeout(function(){
            console.log('异步任务1执行完成');
            resolve('成功 1');
        }, 1000);
    });
    return p;            
}
function run2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务2执行完成');
            resolve('成功 2');
        }, 2000);
    });
    return p;            
}
function run3(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务3执行完成');
            resolve('成功 3');
        }, 2000);
    });
    return p;            
}


我们上面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。


function Number(){
    var p = new Promise(function(resolve, reject){
       
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}


Number()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);




运行Number并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:resolved 1   rejected  数字太大了


'''catch方法:'''


 其实跟then方法后面的第二个参数是一样的功能。不过在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么用了catch并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:


Number()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});


在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用catch,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果: resolved 4  rejected  ReferenceError:somedata is not defined


已经进入catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能




'''all用法'''


Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的run1、run2、run3这三个函数,看下面的例子:




Promise
.all([run1(), run2(), run3()])
.then(function(results){
    console.log(results);
});


结果:['成功1','成功2','成功3']


用了all方法,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据










'''三、JQuery Promise'''


Ajax仅支持一个回调函数,但在JQuery的1.5版本中,引入了 Deferred 对象,它允许注册多个回调函数,并且能传递任何同步或异步函数的执行状态–成功或失败。简单说, 、
Deferred 对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。




回顾一下在JQuery 1.5之前,传统的Ajax操作写法:


$.ajax({
url: "/ServerResource.txt",
success: successFunction,
error: errorFunction
});
$.ajax()操作完成后,如果使用的是低于1.5.0版本的jQuery,返回的是XHR对象,你没法进行链式操作;如果高于1.5.0版本,返回的是deferred对象,可以进行链式操作。


现在,新的写法是这样的:




var promise = $.ajax({
url: "/ServerResource.txt"
});
promise.done(successFunction);
promise.fail(errorFunction);
promise.always(alwaysFunction);




在jQuery 1.6之前, always() 相当于 complete() ,在 done() 或 fail() 执行完毕之后才执行,即无论Ajax的执行结果是什么, always() 总会执行。


done() , fail() , 和 always() 会返回同一个JQuery XMLHttpRequest(jqXHR)对象,所以可以进行链式调用:




$.ajax( "example.php" )
.done(function() { alert("success"); })
.fail(function() { alert("error"); })
.always(function() { alert("complete"); });




deferred对象的一大好处,就是它允许你自由添加多个回调函数。




$.ajax("test.html")
.done(function(){ alert("哈哈,成功了!");} )
.fail(function(){ alert("出错啦!"); } )
.done(function(){ alert("第二个回调函数!");} );


回调函数可以添加任意多个,它们按照添加顺序执行。如果在后续的代码中还需要利用改jqXHR对象,就必须用变量保存:


var jqxhr = $.ajax( "example.php" )
.done(function() { alert("success"); })
.fail(function() { alert("error"); })
.always(function() { alert("complete"); });






// 再次调用
jqxhr.always(function() { alert("another complete"); });
另外一种产生链式调用的方式是利用Promise的 then 方法,它接受三个event handlers作为参数,在jquery 1.8之前,对于多个回调函数,有需要以数组方式传入三个参数:




$.ajax({url: "/ServerResource.txt"})
.then([successFunction1, successFunction2, successFunction3],
[errorFunction1, errorFunction2]);




//相当于如下
var jqxhr = $.ajax({
url: "/ServerResource.txt"
});
jqxhr.done(successFunction1);
jqxhr.done(successFunction2);
jqxhr.done(successFunction3);
jqxhr.fail(errorFunction1);
jqxhr.fail(errorFunction2);


1.8版本之后, then 会返回一个新的Promise,它可以通过一个函数过滤掉Deferred对象的状态和值,用于取代不被推荐使用的 deferred.pipe() 方法。




var promise = $.ajax({
url: "/ServerResource.txt"
});
promise.then(successFunction, errorFunction);


//如果不想处理某个事件类型,可以传入Null
var promise = $.ajax({
url: "/ServerResource.txt"
});


promise.then(successFunction); //no handler for the fail() event
then() 方法还能逐次调用多个方法,可以用于处理有着先后顺序或者依赖的多个Ajax请求:


var promise = $.ajax("/myServerScript1");
function getStuff() {
return $.ajax("/myServerScript2");
}
promise.then(getStuff).then(function(myServerScript2Data){
// Do something
});




'''为多个操作指定回调函数'''


上文提到过,Deferred对象允许你为多个事件指定一个回调函数,这是传统写法做不到的。请看下面的代码,它用到了一个新的方法 $.when() :


$.when($.ajax("test1.html"), $.ajax("test2.html"))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
这段代码的意思是,先执行两个操作 $.ajax("test1.html") 和 $.ajax("test2.html") ,如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。


$.when()的参数只能是deferred对象,如果不是,则done会立即执行:


var wait = function(){
var tasks = function(){
alert("执行完毕!");
};
setTimeout(tasks,5000);
};
$.when(wait())
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });




'''Promise的状态'''




在任何时刻,Promise只能处于三种状态之一:未完成(unfulfilled)、已完成(resolved)和已失败(resolved)。promise默认的状态是unresolved,任何处于回调队列的函数都会被执行。举个粟子,如果一个Ajax调用成功, $.resolved 会被调用,同时promise的状态转为resolved,以及任何监听 done 的回调都会被执行;相反,则 $.rejected 会被调用,同时promise的状态转为rejected,以及任何监听 fail 的回调都会被执行。


对上述的wait进行改写:


var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(dtd){
var tasks = function(){
alert("执行完毕!");
dtd.resolve(); // 改变deferred对象的执行状态
//dtd.reject(); 改变Deferred对象的执行状态
};
setTimeout(tasks,5000);
return dtd;
};
现在,wait()函数返回的是deferred对象,这就可以加上链式操作了。


//wait()函数运行完,就会自动运行done()方法指定的回调函数。
$.when(wait(dtd))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
在ajax操作中,deferred对象会根据返回结果,自动改变自身的执行状态;但是,在wait()函数中,这个执行状态必须由程序员手动指定。调用dtd.resolve(),将dtd对象的执行状态从”未完成”改为”已完成”,从而触发done()方法。调用dtd.reject(),将dtd对象的执行状态从”未完成”改为”已失败”,从而触发fail()方法。


但是这种写法是有问题的,因为dtd是一个全局对象,所以它的执行状态可以从外部改变。




var dtd = $.Deferred(); // 新建一个Deferred对象
var wait = function(dtd){
var tasks = function(){
alert("执行完毕!");
dtd.resolve(); // 改变Deferred对象的执行状态
};
setTimeout(tasks,5000);
return dtd;
};
$.when(wait(dtd))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
dtd.resolve(); //在这里改变dtd的执行状态




我在代码的尾部加了一行dtd.resolve(),这就改变了dtd对象的执行状态,因此导致done()方法立刻执行,跳出”哈哈,成功了!”的提示框,等5秒之后再跳出”执行完毕!”的提示框。


为了避免这种情况,可以在内部建立Deferred对象:




var wait = function(dtd){
var dtd = $.Deferred(); // 在内部新建一个Deferred对象
var tasks = function(){
alert("执行完毕!");
dtd.resolve(); // 改变Deferred对象的执行状态
};
setTimeout(tasks,5000);
return dtd;
};




'''另外一个方式是利用 deferred.promise() 方法:'''




也就是说, deferred.promise() 只是阻止其他代码来改变这个 deferred 对象的状态。可以理解成,通过 deferred.promise() 方法返回的 deferred promise 对象,是没有 resolve ,reject, progress , resolveWith, rejectWith , progressWith 这些可以改变状态的方法,你只能使用 done, then ,fail 等方法添加 handler 或者判断状态。


var dtd = $.Deferred(); //新建一个延迟对象
var wait = function(dtd){
var tasks = function(){
alert("执行完毕!");
dtd.resolve(); // 改变Deferred对象的执行状态
};




setTimeout(tasks,5000);
return dtd.promise(); // 返回promise对象
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>