转:JavaScript Promises相当酷:一种有趣的方案库

许多的语言,为了将异步模式处理得更像平常的顺序,都包含一种有趣的方案库,它们被称之为promises,deferreds,或者futures。JavaScript的promises ,可以促进关注点分离,以代替紧密耦合的接口。 本文讲的是基于Promises/A 标准的JavaScript promises。[http://wiki.commonjs.org/wiki/Promises/A]Promise的用例:

  • 执行规则
  • 多个远程验证
  • 超时处理
  • 远程数据请求
  • 动画
  • 将事件逻辑从应用逻辑中解耦
  • 消除回调函数的恐怖三角
  • 控制并行的异步操作

JavaScript promise是一个承诺将在未来返回值的对象。是具有良好定义的行为的数据对象。promise有三种可能的状态:

  1. Pending(待定)
  2. Rejected(拒绝)
  3. Resolved(已完成)

一个已经拒绝或者完成的承诺属于已经解决的。一个承诺只能从待定状态变成已经解决的状态。之后,承诺的状态就不变了。承诺可以在它对应的处理完成之后很久还存在。也就是说,我们可以多次取得处理结果。我们通过调用promise.then()来取得结果,这个函数一直到承诺对应的处理结束才会返回。我们可以灵活的串联起一堆承诺。这些串联起来的“then”函数应该返回一个新的承诺或者最早的那个承诺。

通过这个样式,我们可以像写同步代码一样来写非同步代码。主要是通过组合承诺来实现:

  • 堆栈式任务:多处散落在代码中的,对应同一个承诺。
  • 并行任务:多个承诺返回同一个承诺。
  • 串行任务:一个承诺,然后接着执行另一个承诺。
  • 上面几种的组合。

为什么要这么麻烦?只用基本的回调函数不行吗?

回调函数的问题

回调函数适合简单的重复性事件,例如根据点击来让一个表单有效,或者保存一个REST调用的结果。回调函数还会使代码形成一个链,一个回调函数调用一个REST函数,并为REST函数设置一个新的回调函数,这个新的回调函数再调用另一个REST函数,依此类推。这就形成了一个如图1中的毁灭金字塔。代码的横向增长大于纵向的增长。回调函数看起来很简单,直到我们需要一个结果,而且是立刻就要,马上就用在下一行的计算中。

图1:毁灭金字塔

  1. ‘use strict‘;
  2. var i = 0;
  3. function log(data) {console.log(‘%d %s‘, ++i, data); };
  4. function validate() {
  5. log("Wait for it ...");
  6. // Sequence of four Long-running async activities
  7. setTimeout(function () {
  8. log(‘result first‘);
  9. setTimeout(function () {
  10. log(‘result second‘);
  11. setTimeout(function () {
  12. log(‘result third‘);
  13. setTimeout(function () {
  14. log(‘result fourth‘)
  15. }, 1000);
  16. }, 1000);
  17. }, 1000);
  18. }, 1000);
  19. };
  20. validate();

在图1中,我使用timeout来模拟异步操作。管理异常的方法是痛苦的,很容易玩漏下游行为。当我们编写回调,那么代码组织变得混乱。图2显示了一个模拟验证流可以运行在NodeJS REPL。在下一节,我们将从pyramid-of-doom模式迁移到一个连续的promise。

Figure

  1. ‘use strict‘;
  2. var i = 0;
  3. function log(data) {console.log(‘%d %s‘, ++i, data); };
  4. // Asynchronous fn executes a callback result fn
  5. function async(arg, callBack) {
  6. setTimeout(function(){
  7. log(‘result ‘ + arg);
  8. callBack();
  9. }, 1000);
  10. };
  11. function validate() {
  12. log("Wait for it ...");
  13. // Sequence of four Long-running async activities
  14. async(‘first‘, function () {
  15. async(‘second‘,function () {
  16. async(‘third‘, function () {
  17. async(‘fourth‘, function () {});
  18. });
  19. });
  20. });
  21. };
  22. validate();

在NodeJS REPL执行的结果

  1. $ node scripts/examp2b.js
  2. 1 Wait for it ...
  3. 2 result first
  4. 3 result second
  5. 4 result third
  6. 5 result fourth
  7. $

我曾经遇到一个AngularJS动态验证的情况,根据对应表的值,动态的限制表单项的值。限制项的有效值范围被定义在REST服务上。

我写了一个调度器,根据请求的值,去操作函数栈,以避免回调嵌套。调度器从栈中弹出函数并执行。函数的回调会在结束时重新调用调度器,直到栈被清空。每次回调都记录所有从远程验证调用返回的验证错误。

我认为我写的玩意儿是一种反模式。如果我用Angular的$http调用提供的promise,在整个验证过程中我的思维会更近似线性形式,就像同步编程。平展的promise链是可读的。继续...

使用Promises

图3显示了我将验证改写成promise链的样子。其中采用了kew promise库。Q库同样适用。要使用该库,首先使用npm将kew库导入到NodeJS,然后加载代码到NodeJS REPL。

Figure

  1. ‘use strict‘;
  2. var Q = require(‘kew‘);
  3. var i = 0;
  4. function log(data) {console.log(‘%d %s‘, ++i, data); };
  5. // Asynchronous fn returns a promise
  6. function async(arg) {
  7. var deferred = Q.defer();
  8. setTimeout(function () {
  9. deferred.resolve(‘result ‘ + arg);\
  10. }, 1000);
  11. return deferred.promise;
  12. };
  13. // Flattened promise chain
  14. function validate() {
  15. log("Wait for it ...");
  16. async(‘first‘).then(function(resp){
  17. log(resp);
  18. return async(‘second‘);
  19. })
  20. .then(function(resp){
  21. log(resp);
  22. return async(‘third‘)
  23. })
  24. .then(function(resp){
  25. log(resp);
  26. return async(‘fourth‘);
  27. })
  28. .then(function(resp){
  29. log(resp);
  30. }).fail(log);
  31. };
  32. validate();

输出和使用嵌套回调时相同:

  1. $ node scripts/examp2-pflat.js
  2. 1 Wait for it ...
  3. 2 result first
  4. 3 result second
  5. 4 result third
  6. 5 result fourth
  7. $

该代码稍微“长高”了,但我认为更易于理解和修改。更易于加上适当的错误处理。在链的末尾调用fail用于捕获链中错误,但我也可以在任何一个then里面提供一个reject的处理函数做相应的处理。

服务器 或 浏览器

Promises在浏览器中就像在NodeJS服务器中一样有效。下面的地址, http://jsfiddle.net/mauget/DnQDx/,指向JSFiddle的一个展示如何使用一个promise的web页面。 JSFiddle所有的代码是可修改的。浏览器输出的一个变化如图4所示。我故意操作随意动作。你可以试几次得到相反的结果。它是可以直接扩展到多个promise链, 就像前面NodeJS例子。

图4.单个promise

并行 Promises

考虑一个异步操作喂养另一个异步操作。让后者包括三个并行异步行为,反过来,喂最后一个行动。只有当所有平行的子请求通过才能通过。如图5所示。这是灵感来自偶遇一打MongoDB操作。有些是合格的并行操作。我实现了promises的流流程图。

图5:异步操作的结构

我们怎么会模拟那些在该图中心行的并行promises?关键是,最大的promise库有一个全功能,它产生一个包含一组子promises的父promie。当所有的子promises通过,父promise通过。如果有一个子promise拒绝,父promise拒绝。

图6显示了一个代码片段,让十个并行的promises每个都包含一个文字promise。只有当十个子类通过或如果任何子类拒绝,最后的then方法才能完成。

  1. var promiseVals = [‘To ‘, ‘be, ‘, ‘or ‘,
  2. ‘not ‘, ‘to ‘, ‘be, ‘, ‘that ‘,
  3. ‘is ‘, ‘the ‘, ‘question.‘];
  4. var startParallelActions = function (){
  5. var promises = [];
  6. // Make an asynchronous action from each literal
  7. promiseVals.forEach(function(value){
  8. promises.push(makeAPromise(value));
  9. });
  10. // Consolidate all promises into a promise of promises
  11. return Q.all(promises);
  12. };
  13. startParallelActions ().then( . . .

下面的地址, http://jsfiddle.net/mauget/XKCy2/,针对JSFiddle在浏览器中运行十个并行promises,随机的拒绝或通过。这里有完整的代码用于检查和变化if条件。重新运行,直到你得到一个相反的完成。图7显示了积极的结果。

图7:JSFiddle并行promises样例

孕育 Promise

许多api返回的promise都有一个then函数——他们是thenable。通常我只是通过then处理thenable函数的结果。然而,$q,mpromise,和kew库拥有同样的API用于创建,拒绝,或者通过promise。这里有API文档链接到每个库的引用部分。我通常不需要构造一个promise,除了本文中的包装promise的未知描述和timeout函数。请参考哪些我创建的promises。

Promise库互操作

大多数JavaScript promise库在then级别进行互操作。你可以从一个外部的promise创建一个promise,因为promise可以包装任何类型的值。then可以支持跨库工作。除了then,其他的promise函数则可能不同。如果你需要一个你的库不包含的函数,你可以将一个基于你的库的promise包装到一个新的,基于含有你所需函数的库创建的promise里面。例如,JQuery的promise有时为人所诟病。那么你可以将其包装到Q,$q,mpromise,或者kew库的promise中进行操作。

结语

现在我写了这篇文章,而一年前我却是犹豫要不要拥抱promise的那个。我只是单纯地想完成一项工作。 我不想学习新的API,或是打破我原来的代码(因为误解了promise)。我曾经如此错误地认为!当我下了一点注时,就轻易就赢得了可喜的成果。

在这篇文章中,我已经简单给出了一个单一的promise,promise链,和一个并行的promise的promise的的例子。 Promises不难使用。如果我可以使用它们,任何人都可以。 要查看完整的概念,我支持你点击专家写的参考指南。从Promises/A 的参考开始,从事实上的标准JavaScript的Promise 开始。

如果你还没有直接使用的promise,试一下。下定决心:你会有一个不错的体验。我保证!

– Lou Mauget, [email protected]

参考链接

时间: 2024-10-10 06:54:19

转:JavaScript Promises相当酷:一种有趣的方案库的相关文章

初始JavaScript Promises之二

初始JavaScript Promises之二 上一篇我们初步学习了JavaScript Promises,本篇将介绍Promise如何优雅地进行错误处理以及提升操作node.js风格1的异步方法的逼格,没错就是使用promisify2. 异步编程中的错误处理 人性的.理想的也正如很多编程语言中已经实现的错误处理方式应该是这样: try {     var val = JSON.parse(fs.readFileSync("file.json")); }catch(SyntaxErro

初识JavaScript Promises

JavaScript有很多槽点,嵌套回调怕是千夫所指. 很久之前,我一直使用async来处理JavaScript异步编程中的嵌套回调问题.当然我也大概的了解过一些其它旨在解决这些问题的类库,诸如EventProxy.Jscex.StepJS.thenjs. 当我第一次看到Promises规范的时候,我根本无法理解它所带来的好处.譬如每个初次学习Promises的人都见过如下的示例代码: //callbacks function callback(err, value){ if(err){ //

Html5 页面中 JavaScript 启动调用的三种方法比较

太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS.Android.Html5.Arduino.pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. 首先,来看一下 Html5 页面引用 JavaScript 代码的几种方式: 1.Html5 页面中使用 <script>  标签容纳

110种有趣的游戏和应用

声明:这是一篇译文,查看原文请猛戳http://www.datamation.com/open-source/110-fun-open-source-games-and-apps-1.html. 又一次,我们庆祝夏天的到来,和夏天一同来到的还有一系列开源的高品质游戏.今年的最佳游戏列表在去年的基础上,增加了一些好玩的游戏,同时删除了一些不再处于活跃开发状态的游戏.在这个列表中,你会发现街机类游戏,棋盘类游戏,休闲类游戏,益智类游戏,教育类游戏,第一人称射击类游戏,音乐类游戏,跑酷类游戏,角色扮演

如何使用JavaScript快速的创建一种常用类型的导航组件:sidebar

本文标签: JavaScript小技巧 JavaScript JavaScript函数 JavaScript处理sidebar JavaScript导航组件 sidebar是一种常用类型的导航组件,它可从页面旁侧弹出,覆盖在正常内容上. 假设你的正常内容为: <div id="main"> Placeholder<p> Placeholder<p> Placeholder<p> </div> 现在我们可以在内容内加入sideb

实现JavaScript中继承的三种方式

一.原型链继承  在原型链继承方面,JavaScript与java.c#等语言类似,仅允许单父类继承.prototype继承的基本方式如下: 代码如下: function Parent(){} function Child(){} Child.prototype = new Parent(); 通过对象Child的prototype属性指向父对象Parent的实例,使Child对象实例能通过原型链访问到父对象构造所定义的属性.方法等.  构造通过原型链链接了父级对象,是否就意味着完成了对象的继承

JavaScript与Html的两种结合方式

第一种:直接在html文件中书写JS代码 例子:<script type="text/javascript"> /*JS代码*/ </script> 第二种:在一个单独的文件中书写JS代码,然后在这个html页面中引入这个js文件 例子:<script type="text/javascript" src="js/01.js"></script> 注意事项:在同一个script标签中,使用scrip

Javascript刷新页面的几种方法:

Javascript刷新页面的几种方法:1 history.go(0)2 location.reload()3 location=location4 location.assign(location)5 document.execCommand('Refresh')6 window.navigate(location)7 location.replace(location)8 document.URL=location.href

Javascript刷新页面的几种方法

Javascript刷新页面的几种方法: 1 history.go(0) 2 location.reload() 3 location=location 4 location.assign(location) 5 document.execCommand('Refresh') 6 window.navigate(location) 7 location.replace(location) 8 document.URL=location.href 自动刷新页面的方法: 1.页面自动刷新:把如下代码