深刻理解jQuery、Angular、node中的Promise

最初遇到Promise是在jQuery中,在jQuery1.5版本中引入了Deferred Object,這個異步隊列模塊用於實現異步任務和回調函數的解耦。爲ajax模塊、隊列模塊、ready事件提供基礎功能。在用jQuery操做DOM的時候對Promise的使用欲不夠強烈,最近學習node和Angular,須要用js寫業務邏輯和數據操做代碼的時候這種場景需求就出來了。通常來講事件適合在交互場景中運用,由於用戶的行爲原本就是分散的,而promise這樣的流程控制適合在後臺邏輯中處理業務html

    //jQuery1.5以前
    $.ajax({
        url: 'url',
        success: function(data, status, xhr) {},
        error: function(xhr, status, msg) {},
        complete: function(xhr, status) {}
    });

如今推薦的寫法是:前端

    $.ajax('url')
        .done(function (data, statusText, xhr) { })
        .done(function (data, statusText, xhr) { })
        .fail(function (data, statusText, error) { })
        .fail(function (data, statusText, error) { })
        .complete(function (xhr, statusText) { })
        .complete(function (xhr, statusText) { });

爲何使用Promise

這有什麼區別呢,看上去前者使用回調函數,後者是鏈式語法,還能重複調用。JavaScript的執行都是單線程、異步的,通常有三種方式處理異步編程:回調函數、事件發佈/訂閱、Promise/Defferred 。說白了就是爲了更好的控制函數執行的流程。回調函數使用簡單,但回調使得調用不一致,得不到保證,當依賴於其餘回調時,可能會篡改代碼的流程,這讓調試變得很是難。當寫一個插件或者sdk的時候,回調函數一多麻煩就來了;至於事件,天然是要比回調函數乾淨不少,但事件的場景,相似於通知,A方法執行了,立刻觸發一個A執行了的事件,訂閱者收到通知後立刻去幹活。事件和回調函數都是被動的,且執行狀態沒有區分(究竟是成功仍是失敗),要麼你就要多寫幾個回調或多訂閱幾個事件,結構上和你的流程多是分離的;所以,如你想在你的代碼流程(好比controller/service中)裏面調用某個異步方法,想獲得肯定的結果,那麼你須要一個Promise(承諾)。這個名字也值得玩味,彷佛寓意「說到作到」。node

jQuery中的promise

jquery的異步隊列包含jQuery.Callbacks(flag),jQuery.Deferred(func)和jQuery.when()。而deferred基於callbacks實現,jquery.when()基於前二者。promise是異步隊列的一個只讀副本jquery

jQuery.Callbacks(flag)

jQuery.Callbacks(flag)返回一個鏈式工具對象,用於管理一組回調函數,內部經過一個數組來保存回調函數,其餘方法圍繞這個數組進行操做和檢測。提供了add()、remove()、fireWith/fire()/fired()、disable()/disabled()、lock/locked方法。git

callbacks.add() 用於添加一個或一組回調函數到回調函數列表中
 callbacks.remove()   用於從回調函數列表中移除一個或一組回調函數  
 callbacks.fireWith(context,args)  使用指定的上下文和參數觸發回調函數列表中的全部回調函數 
 callbacks.fire() 使用指定的參數觸發回調函數中的全部回調函數
 callbacks.fired()  用於判斷函數列表是否被觸發過
 callbacks.disable()   禁用回調函數列表     
 callbacks.disabled() 用於判斷回調函數是否被禁用 
 callbacks.lock() 用於鎖定回調函數(鎖定memory模式下的回調函數的上下文和參數)
 callback.locked() 用於判斷是否被鎖定 

 能夠跑一下這些示例加深理解:http://www.cnblogs.com/lmule/p/3463515.html 這就不贅述了。github

jQuery.Deferred(func)

jQuery.Deferred在jQuery.Callbacks(flags)的基礎上爲回調函數增長了三種狀態:Pending(待定),resolved(成功)和rejected(失敗)。它內部維護了三個回調函數列表,分別是成功回調函數列表、失敗回調函數列表和消息回調函數列表。調用方法defferred.resolve(args)和resolveWith(context,args)將改變異步隊列的狀態爲成功狀態,並會當即執行添加的全部成功回調函數。反之,調用deferred.reject和deferred.rejectWith(context,args)會進入失敗狀態並觸發全部失敗的回調函數。一旦進入失敗或成功狀態,就會保持狀態不變。也就是說再次調用deferred.reject()或deferred.resolve()會被忽略.ajax

var tuples = [
                // action, add listener, listener list, final state
                [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
                [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
                [ "notify", "progress", jQuery.Callbacks("memory") ]
            ],
//jquery-1.90

提供的主要方法以下:數據庫

deferred.done(doneCallback[,doneCallback]) 添加成功回調函數,當異步隊列處於成功時調用
deferred.fail(failCallback[,failCallback]) 添加失敗回調函數,當異步隊列處於失敗時調用
deferred.process(callback) 添加消息回調函數  
deferred.then(doneCallback,failCallback,[,processCallback]) 同時添加成功、失敗、消息回調函數。
deferred.always(callback[,callback]) 添加回調函數,當異步隊列處於成功或失敗的時候調用
deferred.resolve(args) 使用指定參數調用全部成功函數,隊列進入成功狀態
deferred.resolveWith(context[,args]) 同上,指定了上下文。
deferred.reject(args) 使用指定參數執行全部失敗回調函數,隊列進入失敗狀態
deferred.rejectWith(context[,args]) 同上,指定了上下文。
deferred.notify(args) 調用全部消息回調函數
deferred.notifyWith(context[,args]) 同上,指定了上下文。
deferred.state() 判斷異步隊列狀態  
deferred.promise([taget]) 返回當前deferred對象的只讀副本,或者爲普通對象增長異步隊列的功能

所謂只讀副本就是隻暴露了添加回調函數和判斷方法,done(),fail(),then()等,而不包含觸發執行和改變狀態的方法:resolve(),rejecet(),notify()等。也就是說,你把事情(回調)交代給promised對象,而後它去作就好了。npm

jQuery.when(deferreds)

jQuery.when方法提供了一個或多個對象來執行回調函數的功能,一般是具備異步事件的異步隊列。也就是說它能夠支持同時給多個耗時的操做添加回調函數。編程

   var doneCallback = function () { console.log('done', arguments); }
    var failCallback = function () { console.log('fail', arguments); }
    $.when($.ajax('/Home/Test?id=1'), $.ajax('/Home/Test?id=2')).done(doneCallback).fail(failCallback);

若是都執行成功會進入到done,只要有失敗就會進入fail。jQuery.when()都會返回它的只讀副本。也就是一個promise。

更多示例能夠移步:阮一峯的博客:jQuery的deferred對象詳解

ajax中的實現

在Ajax中是將jqXHR對象增長了異步隊列的功能,從下面的源碼能夠看到調用done和success是等價的,同理對於fail和error.

  deferred = jQuery.Deferred(),
  completeDeferred = jQuery.Callbacks("once memory"),
   //..

 // Attach deferreds
        deferred.promise( jqXHR ).complete = completeDeferred.add;
        jqXHR.success = jqXHR.done;
        jqXHR.error = jqXHR.fail;

jqxhr:

 當ajax執行完後,觸發回調函數:

        // Success/Error
            if ( isSuccess ) {
                deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
            } else {
                deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
            }

            // Status-dependent callbacks
            jqXHR.statusCode( statusCode );
            statusCode = undefined;

            if ( fireGlobals ) {
                globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
                    [ jqXHR, s, isSuccess ? success : error ] );
            }

            // Complete
            completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );

ajax的例子就不用說了,開篇就是。

隊列中的實現

隊列是一種先進先出的數據結構,jQuery的隊列提供了.queue()/jQuery.queue()和dequeue()/jQuery.deueue()實現入隊和出隊操做,這些是基於數據緩存模塊和數組實現。隊列提供了一個promise(type,object)方法,返回一個異步隊列的只讀副本,來觀察對應的隊列是否執行完成。

   // Get a promise resolved when queues of a certain type
    // are emptied (fx is the type by default)
  // jquery-1.90
promise: function( type, obj ) { var tmp, count = 1, defer = jQuery.Deferred(), elements = this, i = this.length, resolve = function() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } }; if ( typeof type !== "string" ) { obj = type; type = undefined; } type = type || "fx"; while( i-- ) { tmp = jQuery._data( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); } } resolve(); return defer.promise( obj ); }

運用:

<div id="queue" style=" background-color: wheat;color: green;width: 200px;">queue</div>
  $("#queue").fadeOut(800).delay(1200).fadeIn().animate({ width: 100 }).animate({ height: 100 })
        .promise().done(function() {
            console.log('done!');
        });

默認狀況下,animate和fade都進入了隊列,全部是按順序執行的。經過設置queue可讓animate的動畫當即執行。

 $("#queue").fadeIn(800).delay(1200).fadeOut().animate({ width: 100 }, { queue: false }).animate({ height: 100 }, { queue: false })
    .promise().done(function() {
            console.log('done!');
        })

若是是多個元素動畫:

  <div id="queue" style="display: none;background-color: wheat;color: green;width: 200px;">queue</div>
        <div id="queue1" style="background-color: wheat;color: green;width: 200px;">queue</div>
        <div id="queue2" style="background-color: wheat;color: green;width: 200px;">queue</div>
$("#queue").fadeOut(800,function() {
        console.log("fadeout,do some thing");
        $("#queue1").animate({ width: '100px' }, 2000, function() {
            console.log("queue1,do some thing");
            $('#queue2').animate({
                height: 100
            }, 2000,function() {
                console.log("queue2,do some thing");
            });
        });
    })

一個嵌套一個,有麼有感受像是麻花。能夠用promise改形成這樣:

   var p1 = $("#queue").fadeOut(800).promise();
    var p2 = $("#queue1").animate({ width: 100 }, 2000).promise();
    var p3 = $("#queue2").animate({ height: 100 }, 2000).promise();
    $.when(p1).then(p2).then(p3);

是否是很清爽。自定義的正確格式是:

   var async = function (key) {
        var deferred = $.Deferred();

        function dowork() {
            if (key == 1) {
                deferred.resolve("success!");
            } else {
                deferred.reject("fail!");
            }
        }
        setTimeout(dowork, 2000);

        return deferred.promise();
    }

就是三步,獲取一個deferred,使用resolve/rejcet在內部決定狀態,而後返回promise,調用:

async(2).done(function (res) {
        console.log(res);
    }).fail(function (res) {
        console.log(res);
    });

有了上面這些,咱們再看去看看Angular和node中的promise。 

Angular中的promise

 promise 是一種用異步方式處理值的方法,promise是對象,表明了一個函數最終可能的返回值或拋出的異常。在與遠程對象打交道很是有用,能夠把它們當作一個遠程對象的代理。

要在Angular中建立promise須要使用內置的$q服務。先用factory定義一個服務,注入$q服務。

angular.module('readApp').factory('asyncService', [
    "$q", function ($q) {
         var myAsync=function(flag) {
            var deferred = $q.defer();
            if (flag) {
                deferred.resolve("well done!");
            } else {
                deferred.reject("lost!");
            }
             return deferred.promise;
         }
        return {
            myAsync: myAsync
        };
    }
]);

得到deferred的方法和jquery不一樣,但resolve和reject是同樣的,最後返回的是promise屬性,而不是promise方法。再看如何調用:

angular.module('readApp').controller('testCtrl', ["$scope", "asyncService", function ($scope, asyncService) {
        $scope.flag = true;
        $scope.handle = function () {
            asyncService.myAsync($scope.flag).then(function (result) {
                $scope.status = result;
                return result;
            }, function (error) {
                $scope.status = error;
                return error;
            });
        }
    }])

獲取到服務後,調用then方法。then有三個參數,分別對應成功回調、失敗回調和通知回調。這個和jquery是一致的。

html:

<div  class="container">
    <label for="flag">成功
        <input type="checkbox" id="flag" ng-model="flag" name="name" /> <br />
        <div>{{status}}</div>
        <button ng-click="handle()">點擊</button>
    </label>
</div>
<footer-n

結果:

不一樣的是,Angular的promise沒有公佈jquery那麼多方法,咱們能夠看一下deferred.promise這個屬性,它是一個$$state對象。根據Promise/A規範,一個Promise只要具有一個then方法便可。

注意到,Angular中的deferred有notify、reject、resolve三個主要方法和一個promise屬性,而這個promise的原型連中包含了咱們調用的then方法,then方法在執行完以後會派生一個新的promise,所以能夠鏈式調用。沒有done和fail,可是還提供了catch和finally方法。catch就至關因而error方法了。而finally方法就像強類型語言中的場景同樣,當咱們須要釋放一個資源,或者是運行一些清理工做,無論promise是成功仍是失敗時,這個方法會頗有用。要注意的是finally是ie中的一個保留字,須要下面這樣調用:

promise['finally'](function() {});

除了defer()方法,$q還有all和when方法,all(promises)能夠將多個promise合併成一個,但若是任意一個promise拒絕了,那麼結果的promise也會拒絕。而when(value)方法把一個多是值或者promise包裝成一個$q promise。有了jQuery中的when,這兩個方法不難理解。關於這三個方法的示例能夠參考這篇博客:AngularJS 中的Promise --- $q服務詳解

Angular的$q的靈感是來自[Kris Kowal's Q],從官方的註釋中能夠看到

 * This is an implementation of promises/deferred objects inspired by
 * [Kris Kowal's Q](https://github.com/kriskowal/q).

  * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
  * implementations, and the other which resembles ES6 promises to some degree.

支持兩種風格,能夠像Q庫或者jQuery的deferred同樣,也能夠用ES6語法,文檔給出了示例,也是就構造函數法來定義:

 var asyncGreet = function (name) {
            return $q(function (resolve, reject) {
                console.log(resolve, reject);
                setTimeout(function () {
                    if (name=="stone") {
                        resolve('Hello, ' + name + '!');
                    } else {
                        reject('Greeting ' + name + ' is not allowed.');
                    }
                }, 1000);
            });
        };

通知(notify/progress)回調還不支持這種寫法。對比看,沒太大差異。

  function asyncGreet(name) {
    var deferred = $q.defer();
    setTimeout(function() {
     deferred.notify('About to greet ' + name + '.');
     if (okToGreet(name)) {
        deferred.resolve('Hello, ' + name + '!');
      } else {
        deferred.reject('Greeting ' + name + ' is not allowed.');
      }
     }, 1000);
   return deferred.promise;
  }

 大體看下源碼如何實現:

 Promise:

 function Promise() {
    this.$$state = { status: 0 };
  }

  extend(Promise.prototype, {
    then: function(onFulfilled, onRejected, progressBack) {
      if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
        return this;
      }
      var result = new Deferred(); this.$$state.pending = this.$$state.pending || [];
      this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
      if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); return result.promise;
    },

    "catch": function(callback) {
      return this.then(null, callback);
    },

    "finally": function(callback, progressBack) {
      return this.then(function(value) {
        return handleCallback(value, true, callback);
      }, function(error) {
        return handleCallback(error, false, callback);
      }, progressBack);
    }
  });

建立了一個Promise對象包含一個$$state屬性,而後擴展了then,catch,finally方法(注意後兩個帶了引號)。then的三個參數都是回調函數,對應成功、失敗、通知回調,並在then方法中建立了一個deferred做爲結果,將回調函數和建立的deferred都存入了數組,主意到這是一個二維數組,每一個then對應的promise和回調函數都在這個數組裏面。最後返回promise。而catch和finally內部也是調用的then。只要狀態大於0也就promise得到告終果就用scheduleProcessQueue處理回調。 Deferred 內部包含了一個promise以及resolve、reject和notify三個方法。jQuery.deferred 中處理的是三個回調隊列,Angular中處理的一個是二維數組。 

$http的是一個promise對象:

  var promise = $q.when(config);
       //some code
       
        promise = promise.then(thenFn, rejectFn);
      }
      if (useLegacyPromise) {
        promise.success = function(fn) {
          assertArgFn(fn, 'fn');
          promise.then(function(response) {
            fn(response.data, response.status, response.headers, config);
          });
          return promise;
        };
        promise.error = function(fn) {
          assertArgFn(fn, 'fn');

          promise.then(null, function(response) {
            fn(response.data, response.status, response.headers, config);
          });
          return promise;
        };
      } else {
        promise.success = $httpMinErrLegacyFn('success');
        promise.error = $httpMinErrLegacyFn('error');
      }

      return promise;

用then擴展了error和succes方法,所以咱們能夠這樣使用:

 booksData.getbookById(bookid).success(function(data) {
            vm.book = data;
        }).error(function (e) {
            console.log(e);
            vm.message = "Sorry, something's gone wrong ";
        });

$Interval也是一個promise對象。

Node中的promise

Q

 node中有不少耗時的回調寫法,好比寫入文件,請求api,查詢數據庫。node比較早的就是Q庫了,也就是angular中參考的版本,先安裝Q:

npm install q --save

改寫一個寫入文件的方法:

var Q = require('q');
var writeFile= function() {
    var deferred = Q.defer();
    fs.writeFile('public/angular/readApp.min.js', uglified.code, function (err) {
        if (err) {
            console.log(err);
            deferred.reject(err);
        } else {
            deferred.resolve('腳本生產並保存成功: readApp.min.js');  }
    });
    return deferred.promise;
}
writeFile().then(function(res) {
    console.log(res);
});

風格上和jQuery、angular如出一轍。但Q的功能很強大,除了咱們如今能想到的Q.all(),Q.when(),還提供了Q.any(),Q.delay(),Q.fcall()等方法。你們能夠直接去看文檔和源碼. 

Q庫文檔:https://github.com/kriskowal/q

Q庫的源碼:https://github.com/kriskowal/q/blob/v1/design/q7.js

 bluebird

 這個庫是從i5ting的博客看到的,這個庫強先後端都能用,並且兼容老ie。使用起來很簡單。安裝:

npm insatll bluebird --save

好比在前端定義一個promise:

function promptPromise(message) {
  return new Promise(function(resolve, reject) {
    var result = window.prompt(message);
    if (result != null) {
      resolve(result);
    } else {
      reject(new Error('User cancelled'));
    }
  });
}

在後端直接promise整個庫: 

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require("fs"));
fs.readFileAsync("nodemon.json").then(function(json) {
    console.log("Successful json", json);
}).catch(SyntaxError, function (e) {
    console.error("file contains invalid json");
});

調用promisifyAll方法後,fs對象就擁有了readFileAsync方法。 同理能夠用到mongoose對象上:

var User = mongoose.model('User', userSchema);
Promise.promisifyAll(User);
 
User.findAsync({ username: "stoneniqiu" }).then(function (data) {
    console.log("success!");
}).catch(function(err) {
    console.log(err);
});

相對於咱們以前的不斷嵌套的寫法這樣簡潔不少了。

甚至誇張的能夠promise全部庫:

var ParanoidLib = require("...");
var throwAwayInstance = ParanoidLib.createInstance();
Promise.promisifyAll(Object.getPrototypeOf(throwAwayInstance));

但這種作法讓人有點擔憂,有興趣的能夠去官網和git上看看。

官網:http://bluebirdjs.com/docs/why-bluebird.html

github:https://github.com/petkaantonov/bluebird

 小結:node中支持promise的庫不少,就不一一例舉了,回到正題借用i5ting的總結:

promise/a+的四個要點

  •  異步操做的最終結果,儘量每個異步操做都是獨立操做單元
  •  與Promise最主要的交互方法是經過將函數傳入它的then方法(thenable)
  •  捕獲異常catch error
  •  根據reject和resolve重塑流程

以前對promise的感受是影影綽綽,今天橫向整理了這篇文章後,清晰了不少了,雖然都是JavaScript代碼,但徹底是不一樣的應用場景和實現方式,無論怎樣promise的本質是同樣的,封裝的越強,功能也越多。從jquery、Angular以及node的這些示例中能夠看出promise在異步編程中應用的很普遍,值得學習。但願本文對你有幫助,若是有不當之處,歡迎拍磚。

參考連接:

http://wiki.commonjs.org/wiki/Promises

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

http://cnodejs.org/topic/560dbc826a1ed28204a1e7de

參考書籍:《jQuery技術內幕》 《Angular權威教程》《深刻淺出Nodejs》

相關文章
相關標籤/搜索