首页 > 代码库 > 深入理解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) { });
为什么使用Promise
这有什么区别呢,看上去前者使用回调函数,后者是链式语法,还能重复调用。JavaScript的执行都是单线程、异步的,一般有三种方式处理异步编程:回调函数、事件发布/订阅、Promise/Defferred 。说白了就是为了更好的控制函数执行的流程。回调函数使用简单,但回调使得调用不一致,得不到保证,当依赖于其他回调时,可能会篡改代码的流程,这让调试变得非常难。当写一个插件或者sdk的时候,回调函数一多麻烦就来了;至于事件,自然是要比回调函数干净很多,但事件的场景,类似于通知,A方法执行了,马上触发一个A执行了的事件,订阅者收到通知后马上去干活。事件和回调函数都是被动的,且执行状态没有区分(到底是成功还是失败),要么你就要多写几个回调或多订阅几个事件,结构上和你的流程可能是分离的;因此,如你想在你的代码流程(比如controller/service中)里面调用某个异步方法,想得到确定的结果,那么你需要一个Promise(承诺)。这个名字也值得玩味,似乎寓意“说到做到”。
jQuery中的promise
jquery的异步队列包含jQuery.Callbacks(flag),jQuery.Deferred(func)和jQuery.when()。而deferred基于callbacks实现,jquery.when()基于前两者。promise是异步队列的一个只读副本。
jQuery.Callbacks(flag)
jQuery.Callbacks(flag)返回一个链式工具对象,用于管理一组回调函数,内部通过一个数组来保存回调函数,其他方法围绕这个数组进行操作和检测。提供了add()、remove()、fireWith/fire()/fired()、disable()/disabled()、lock/locked方法。
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 这就不赘述了。
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()会被忽略.
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对象,然后它去做就行了。
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》
深入理解jQuery、Angular、node中的Promise