這次帶給大家jQuery、Angular、node中的Promise詳解,使用jQuery、Angular、node中Promise的注意事項有哪些,下面就是實戰案例,一起來看一下。
最初遇到Promise是在jQuery中,在jQuery1.5版本中引入了Deferred Object,這個非同步佇列模組用於實現非同步任務和回呼函數的解耦。為ajax模組、佇列模組、ready事件提供基礎功能。在用jQuery操作DOM的時候對Promise的使用慾不夠強烈,最近學習node和Angular,需要用js寫業務邏輯和資料操作程式碼的時候這種場景需求就出來了。 一般來說事件適合在互動場景中運用,因為使用者的行為本來就是分散的,而promise這樣的流程控制適合在後台邏輯中處理業務。
//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) { });
這有什麼差別呢,看上去前者使用回呼函數,後者是鍊式語法,還能重複呼叫。 JavaScript的執行都是單執行緒、非同步的,一般有三種方式處理非同步程式設計:回呼函數、事件發布/訂閱、Promise/Defferred 。說白了就是為了更好的控制函數執行的流程。回調函數使用簡單,但回調使得呼叫不一致,得不到保證,當依賴其他回調時,可能會篡改程式碼的流程,這讓偵錯變得非常難。當寫一個外掛程式或sdk的時候,回呼函數一多麻煩就來了;至於事件,自然是要比回呼函數乾淨很多,但事件的場景,類似於通知,A方法執行了,馬上觸發一個A執行了的事件,訂閱者收到通知後馬上去工作。事件和回呼函數都是被動的,且執行狀態沒有區分(到底是成功還是失敗),要么你就要多寫幾個回調或多訂閱幾個事件,結構上和你的流程可能是分離的;因此,如你想在你的程式碼流程(例如controller/service中)裡面呼叫某個非同步方法,想得到確定的結果,那麼你需要一個Promise(承諾)。這個名字也值得玩味,似乎寓意「說到做到」。
jquery的非同步佇列包含jQuery.Callbacks(flag),jQuery.Deferred(func)和jQuery.when()。而deferred則是基於callbacks實現,jquery.when()是基於前兩者。 promise是非同步隊列的一個只讀副本。
jQuery.Callbacks(flag)傳回一個鍊式工具對象,用於管理一組回呼函數,內部透過一個陣列來保存回呼函數,其他方法圍繞這個數組進行操作和檢測。提供了add()、remove()、fireWith/fire()/fired()、disable()/disabled()、lock/locked方法。
callbacks.add() | 用於新增一個或一組回呼函數到回呼函數清單中 |
callbacks.remove() | 用於從回呼函數清單中移除一個或一組回呼函數 |
callbacks.fireWith(context,args) | |
使用指定的上下文和參數觸發回呼函數清單中的所有回呼函數 | |
使用指定的參數觸發回呼函數中的所有回呼函數 | |
用於判斷函數列表是否被觸發過 | |
停用回呼函數清單 | |
用於判斷回呼函數是否已停用 | |
用於鎖定回呼函數(鎖定memory模式下的回呼函數的上下文和參數) |
可以跑一下这些示例加深理解:http://www.cnblogs.com/lmule/p/3463515.html 这就不赘述了。
jQuery.Deferred在jQuery.Callbacks(flags)的基础上为回调函数增加了三种状态:Pending(待定),resolved(成功)和rejected(失败)。它内部维护了三个回调函数列表,分别是成功回调函数列表、失败回调函数列表和消息回调函数列表。调用方法defferred.resolve(args)和resolveWith(context,args)将改变异步队列的状态为成功状态,并会立即执行添加的所有成功回调函数。反之,调用deferred.reject和deferred.rejectWith(context,args)会进入失败状态并触发所有失败的回调函数。一旦进入失败或成功状态,就会保持状态不变。也就是说再次调用deferred.reject()或deferred.resolve()会被忽略.
tuples = [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved""reject", "fail", jQuery.Callbacks("once memory"), "rejected""notify", "progress", jQuery.Callbacks("memory"
提供的主要方法如下:
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对象,然后它去做就行了。
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中是将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)方法,返回一个异步队列的只读副本,来观察对应的队列是否执行完成。
promise: = 1== = = ( !( -- ( type !== "string"=== type || "fx"( i--= jQuery._data( elements[ i ], type + "queueHooks" ( tmp &&++
运用:
<p id="queue" style=" background-color: wheat;color: green;width: 200px;">queue</p>
$("#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!'); })
如果是多个元素动画:
<p id="queue" style="display: none;background-color: wheat;color: green;width: 200px;">queue</p> <p id="queue1" style="background-color: wheat;color: green;width: 200px;">queue</p> <p id="queue2" style="background-color: wheat;color: green;width: 200px;">queue</p>
$("#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。
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:
<p class="container"> <label for="flag">成功 <input type="checkbox" id="flag" ng-model="flag" name="name" /> <br /> <p>{{status}}</p> <button ng-click="handle()">点击</button> </label></p><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中有很多耗时的回调写法,比如写入文件,请求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
这个库是从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在非同步程式設計中應用的很廣泛,值得學習。希望這篇文章對你有幫助,如果有不當之處,歡迎拍磚。
相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
#以上是jQuery、Angular、node中的Promise詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!