JavaScript Promises 相当酷 已翻译 100%

oschina 投递于 2014/07/30 06:31 (共 12 段, 翻译完成于 09-02)
阅读 8364
收藏 125
8
加载中

“And when I promise something, I never ever break that promise. Never.” ― Rapunzel

许多的语言,为了将异步模式处理得更像平常的顺序,都包含一种有趣的方案库,它们被称之为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(已完成)

郭振华oo
郭振华oo
翻译于 2014/08/29 18:27
2

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

  • 堆栈式任务:多处散落在代码中的,对应同一个承诺。

  • 并行任务:多个承诺返回同一个承诺。

  • 串行任务:一个承诺,然后接着执行另一个承诺。

  • 上面几种的组合。

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

f4f
f4f
翻译于 2014/09/01 20:06
1

回调函数的问题

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

'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };

function validate() {
   log("Wait for it ...");
   // Sequence of four Long-running async activities
   setTimeout(function () {
      log('result first');
      setTimeout(function () {
         log('result second');
         setTimeout(function () {
            log('result third');
            setTimeout(function () {
               log('result fourth')
            }, 1000);
         }, 1000);
      }, 1000);
   }, 1000);

};
validate();

f4f
f4f
翻译于 2014/09/01 20:27
2

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

'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };

// Asynchronous fn executes a callback result fn
function async(arg, callBack) {
   setTimeout(function(){
      log('result ' + arg);
      callBack();
   }, 1000);
};

function validate() {
   log("Wait for it ...");
   // Sequence of four Long-running async activities
   async('first', function () {
      async('second',function () {
         async('third', function () {
            async('fourth', function () {});
         });
      });
   });
};
validate();

在NodeJS REPL执行的结果

$ node scripts/examp2b.js
1 Wait for it ...
2 result first
3 result second
4 result third
5 result fourth
$
多多de棉花糖
多多de棉花糖
翻译于 2014/08/08 14:56
1

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

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

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

郭振华oo
郭振华oo
翻译于 2014/09/02 15:58
1

使用Promises

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

'use strict';
var Q = require('kew');
var i = 0;

function log(data) {console.log('%d %s', ++i, data); };

// Asynchronous fn returns a promise
function async(arg) {
	var deferred = Q.defer();
	setTimeout(function () {
		deferred.resolve('result ' + arg);\
	}, 1000);
	return deferred.promise;
};

// Flattened promise chain
function validate() {
	log("Wait for it ...");
	async('first').then(function(resp){
		log(resp);
		return async('second');
	})
	.then(function(resp){
		log(resp);
		return async('third')
	})
	.then(function(resp){
		log(resp);
		return async('fourth');
	})
	.then(function(resp){
		log(resp);
	}).fail(log);
};
validate();

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

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

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

郭振华oo
郭振华oo
翻译于 2014/09/02 16:20
1

服务器 或 浏览器

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

Figure 4

图4.单个promise

多多de棉花糖
多多de棉花糖
翻译于 2014/08/07 18:38
1

并行 Promises

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

Figure 5

图5:异步操作的结构

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

多多de棉花糖
多多de棉花糖
翻译于 2014/08/07 20:01
1

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

	var promiseVals = ['To ', 'be, ', 'or ',
		'not ', 'to ', 'be, ', 'that ',
		'is ', 'the ', 'question.'];

	var startParallelActions = function (){
		var promises = [];

		// Make an asynchronous action from each literal
		promiseVals.forEach(function(value){
			promises.push(makeAPromise(value));
		});

		// Consolidate all promises into a promise of promises
		return Q.all(promises);
	};

	startParallelActions ().then( . . .

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

Figure 7

图7:JSFiddle并行promises样例

多多de棉花糖
多多de棉花糖
翻译于 2014/08/08 08:44
1

孕育 Promise

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

多多de棉花糖
多多de棉花糖
翻译于 2014/08/08 10:34
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(18)

James7
James7

引用来自“Windoze”的评论

哈哈,if/else分支咋办?一分叉就要嵌套鸟?合着这个“很酷”的promise只适用于平铺的程序,果然很酷,嗯。
稍微转变下思路,if/else分支在异步调用的函数内处理就解决了。 function loadData($q, $http, params) { var deferred = $q.defer(); if(params.urlA) { $http.get(params.urlA) .success( function(data) { deferred.resolve(data); }) .error( function(reason) { deferred.reject(reason); }); } else if(params.urlB) { $http.get(params.urlB) .success( function(data) { deferred.resolve(data); }) .error( function(reason) { deferred.reject(reason); }); } else { deferred.resolve(null); } return deferred.promise; };
sikele
sikele

引用来自“starstroll”的评论

感觉更难理解了,我花了大概20分钟去看这遍文章思考它的原理和理念,但还是感觉很模糊。
学习成本如此之高带来的只是写此类代码的另一种方式,无性能提升,无复杂性降低,还多引用了一个库,这是何必呢?

引用来自“sikele”的评论

因为新的浏览器内置原生支持了,他能一定程度上防止callback地狱平铺回调,他是将来async await写法的基础。

引用来自“starstroll”的评论

需要低版本的浏览器支持了才能叫内置,否则还不是要引包?
引包你不会选择性引么。。。先进的浏览器都不需要引了。
钛钍
钛钍
很酷的功能,adobe brackets 中无处不在的deferred,才开始了解deferred
Windoze
Windoze
哈哈,if/else分支咋办?一分叉就要嵌套鸟?合着这个“很酷”的promise只适用于平铺的程序,果然很酷,嗯。
ruki
ruki
promise只适合那些简单的异步逻辑处理。复杂的异步函数 你还是得回调 不然一个函数顺序联合多个复杂异步操作 看的得累死
lxbzmy
lxbzmy
觉得难用,并不是说原来ajax的地方非要用promise.而是在一些多层异步的场合使用promise,会简化程序流,使之阅读自然.
starstroll
starstroll

引用来自“starstroll”的评论

感觉更难理解了,我花了大概20分钟去看这遍文章思考它的原理和理念,但还是感觉很模糊。
学习成本如此之高带来的只是写此类代码的另一种方式,无性能提升,无复杂性降低,还多引用了一个库,这是何必呢?

引用来自“sikele”的评论

因为新的浏览器内置原生支持了,他能一定程度上防止callback地狱平铺回调,他是将来async await写法的基础。
需要低版本的浏览器支持了才能叫内置,否则还不是要引包?
已注销用户
已注销用户
讲的是什么?
sikele
sikele

引用来自“starstroll”的评论

感觉更难理解了,我花了大概20分钟去看这遍文章思考它的原理和理念,但还是感觉很模糊。
学习成本如此之高带来的只是写此类代码的另一种方式,无性能提升,无复杂性降低,还多引用了一个库,这是何必呢?
因为新的浏览器内置原生支持了,他能一定程度上防止callback地狱平铺回调,他是将来async await写法的基础。
starstroll
starstroll
感觉更难理解了,我花了大概20分钟去看这遍文章思考它的原理和理念,但还是感觉很模糊。
学习成本如此之高带来的只是写此类代码的另一种方式,无性能提升,无复杂性降低,还多引用了一个库,这是何必呢?
返回顶部
顶部