迄今为止,可能每个JavaScript开发者和他们的祖母都听说过Promises。如果你没有,那么你即将会。promises的概念是由CommonJS小组的成员在 Promises/A规范 中提出来的。Promises被逐渐用作一种管理异步操作回调的方法,但出于它们的设计,它们远比那个有用得多。事实上,由于它们的多种用法,有无数人告诉我——在我写过一些关于promises的东西后——我“遗漏了promises的重点”。那么什么是promises的重点呢?
一点关于Promises的东西
在我开始promise的“重点”之前,我想我应该给你一点它们如何工作的内貌。一个promise是一个对象——根据Promise/A规范——只需要一个方法:then。then方法带有三个参数:一个成功回调,一个失败回调,和一个前进回调(规范没有要求包括前进回调的实现,但是很多都实现了)。一个全新的promise对象从每个then的调用中返回。
一个promise可以是三种状态之一:未完成的,完成的,或者失败的。promise以未完成的状态开始,如果成功它将会是完成态,如果失败将会是失败态。当一个promise移动到完成态,所有注册到它的成功回调将被调用,而且会将成功的结果值传给它。另外,任何注册到promise的成功回调,将会在它已经完成以后立即被调用。
同样的事情发生在promise移动到失败态的时候,除了它调用的是失败回调而不是成功回调。对包含前进特性的实现来说,promise在它离开未完成状态以前的任何时刻,都可以更新它的progress。当progress被更新,所有的前进回调(progress callbacks)会被传递以progress的值,并被立即调用。前进回调被以不同于成功和失败回调的方式处理;如果你在一个progress更新已经发生以后注册了一个前进回调,新的前进回调只会在它被注册以后被已更新的progress调用。
我们不会进一步深入promise状态是如何管理的,因为那不在规范之内,而且每个实现都有差别。在后面的例子中,你将会看到它是如何完成的,但目前这就是所有你需要知道的。
处理回调
像前面提到的为异步操作处理回调,是promises的最基本和最普通的用途,让我们将一个标准的回调与一个采用了promise的回调比较一下。
回调
- // Normal callback usage
- asyncOperation(function() {
- // Here's your callback
- });
- // Now `asyncOperation` returns a promise
- asyncOperation().then(function(){
- // Here's your callback
- });
我很怀疑只是看到这个例子的话是否有人会真的关心去使用promises。看起来没有什么好处,除了“then”使得在异步操作完成之后的回调函数被调用这件事看起来更加明显。但是即使是这个好处,我们现在有了更多的代码(抽象应该使我们的代码更短,不是吗?)而且promise比标准回调稍微性能差一点。
但是,不要让这阻碍到你。如果这就是promise可以做的最好的事,这篇文章就不会存在了。
厄运的金字塔
网上你可以找到很多文章引用“厄运的金字塔”的说法作为使用promises的主要原因。这是指需要连续的执行多个异步操作。在普通回调下,我们将会在相互的调用之间结束嵌套的调用;随着这种调用代码变得更缩进,生成了一个金字塔(指向右方的)因此有了“厄运的金字塔”的名字。如果你只需连续执行一两个异步操作,那么这还不是太坏,但一旦你需要做3个或更多,它将会变得难以阅读,特别是当每一步都有相当多的处理需要做的时候。使用promises可以帮助代码变平,而且使它再一次变得更容易阅读。我们来看看。
厄运的金字塔
- // Normal callback usage => PYRAMID OF DOOOOOOOOM
- asyncOperation(function(data){
- // Do some processing with `data`
- anotherAsync(function(data2){
- // Some more processing with `data2`
- yetAnotherAsync(function(){
- // Yay we're finished!
- });
- });
- });
- // Let's look at using promises
- asyncOperation()
- .then(function(data){
- // Do some processing with `data`
- return anotherAsync();
- })
- .then(function(data2){
- // Some more processing with `data2`
- return yetAnotherAsync();
- })
- .then(function(){
- // Yay we're finished!
- });
正如你所见,promises的使用使得事情变扁平而且更可读了。这能起作用是因为——像早先提到的——then返回了一个promise,所以你可以将then的调用不停的串连起来。由then返回的promise装载了由调用返回的值。如果调用返回了一个promise(像这个例子中的情形一样),then返回的 promise装载了与你的回调返回的promise所装载的相同值。这内容很多,因此我将帮助你一步一步的理解它。
异步操作返回一个promise对象。因此我们在那个promise对象中调用then,并且传给它一个回调函数;then也会返回一个promise。当异步操作结束,它将给promise装上数据。然后(第一次)回调被调用,数据被作为参数传递进去。如果回调不含有返回值,then返回的promise将会立即不带值组装。如果回调返回的不是一个promise,那么then返回的 promise将会立即装载那个数值。如果回调返回一个promise(像例子中的),那么then返回的 promise将等待直到我们回调返回的promise被完全装载。一旦我们回调的 promise被装载,它装载的值(本例中就是data2)将会被提交给then的promise。然后then中的promise装载了data2。等等。听起来有点复杂,但事实上很简单,如果我说的你不能理解,我非常抱歉。我猜我可能不是谈论它的最佳人选。
用命名的回调替代
但显然 promises 不是使这个结构扁平化的唯一方法。在写了一篇提到promises解决了厄运的金字塔问题的帖子之后,有个人对该帖评论说……
我想promises有时是有用的,但是“嵌套”的回调的问题(圣诞树综合症)可以仅用一个命名的函数作为一个参数替代匿名函数的方法平常的处理:
|
它的例子只是给出了一层深的例子,但它仍是正确的。我们来扩展我前面的例子,使这个看起来容易些。
命名回调
- // Normal callback usage => PYRAMID OF DOOOOOOOOM
- asyncOperation(handler1);
- function handler1(data) {
- // Do some processing with `data`
- anotherAsync(handler2);
- }
- function handler2(data2) {
- // Some more processing with `data2`
- yetAnotherAsync(handler3);
- }
- function handler3() {
- // Yay we're finished!
- }
看看上面的代码!他们绝对是对的!它就是一个扁平的结构,但是这里有个问题同样也存在于 我以前从来没有注意过的老的回调例子中:依赖性和复用性。依赖性和复用性是相互关联的可逆类型。一样东西依赖的越少,那么它的复用性就越大。在以上的例子中,handler1依赖handler2,handler2依赖handler3.这就意味着handler1无论出于任何目的都不可在被用除非handler2也呈现出来。假如你不打算重用他们,那么给你的函数命名又有什么意义呢?
最糟糕的的是handler1都不关心在handler2里面发生了什么事情。它压根就不需要handler2除了和它异步工作。因此,让我们消除这些依赖性然后通过用promise使函数更具复用性。
#p#
链式回调
- asyncOperation().then(handler1).then(handler2).then(handler3);
- function handler1(data) {
- // Do some processing with `data`
- return anotherAsync();
- }
- function handler2(data2) {
- // Some more processing with `data2`
- return yetAnotherAsync();
- }
- function handler3() {
- // Yay we're finished!
- }
这样看起来是不是好多了?假如另外的函数存在的话,现在handler1和handler2都互不相关了。想看看他们是否真的很棒呢?现在handler1可以被用在不需要handler2的情况下了。相反,handler1被操作以后,我们将可以用另一个handler。
复用函数
- asyncOperation().then(handler1).then(anotherHandler);
- function handler1(data) {
- // Do some processing with `data`
- return anotherAsync();
- }
- function anotherHandler(data2) {
- // Do some really awesome stuff that you've never seen before. It'll impress you
- }
现在handler1已经从handler2脱离而且可以被用在了更多的情形中,特别是那些由handler2提供的功能而我们又不想用的。这就是复用性!评论家解决代码易读性的唯一方法就是通过消除缩进。我们不想消除缩进仅仅是为了缩进。多层次的缩进仅仅是某些事情错误的标志,问题不一定在它本身。他就像是由脱水引起的头痛。真正的问题是脱水,不是头痛。解决的方法是获得水合物,而不是用一些止痛药。
并行异步操作
在前面我提到的文章里,我将promises与events在处理异步操作方面做了比较。遗憾的是,按照那些曾提到过的人在评论里给的说法,我比较的不是很成功。我描述出了promises的力量,接着转到events来描述它们的力量,就像在我的特别项目里用到的那样。没有比较和对比。一位评论者写道(修改了一点语法错误):
我想用帖子中的例子是一个坏的对照。有篇论文证明了promises的值将会怎样,如果按下虚构的“启动服务器按钮”,将不仅仅是启动一个web服务器,还有一个数据库服务器,当它们都在运行的时候只是更新了UI。 使用promise的.when方法将会使这种“多个异步操作”例子变得普通,然而响应多个异步事件需要一个并不普通的代码量。 |
他完全正确。事实上我没有比较那两种情况。那篇文章的要点实际在于说明promises不是异步操作的唯一机制,而且在一些情况下,它们也不一定是最好的。在这个评论者指出的情况下,promises当然是最佳的解决办法。我们来看看他说的是什么。
jQuery 具有 一个名为when的方法 ,可以带上任意数量的promise参数,并返回一个单一的promise。如果任何一个promise传入失败,when返回的promise也会失败。如果所有的promises被装载,那么每个值都将会按照promises被定义的顺序传递给附加的回调。
以并行的方式执行无数的异步操作非常有用,然后只要在它们之中的每一个结束之后继续执行回调。我们看一个简单的例子.
jQuery.when
- // Each of these async functions return a promise
- var promise1 = asyncOperation1();
- var promise2 = asyncOperation2();
- var promise3 = asyncOperation3();
- // The $ refers to jQuery
- $.when(promise1, promise2, promise3).then(
- function(value1, value2, value3){
- // Do something with each of the returned values
- }
- );
人们经常说这是 promises 带来的最好的东西之一,也是 promises 的一部分重要的意义所在。我也认为这是个简化了大量操作的好特性,但是这种 when 方法的机制 根本就没有在任何 Promises 规范中提到,所以我不认为它是 Promises意义所在。有一个规范提到了 when 方法,但是和上面的完全不同。就我所知,jQuery 是唯一的实现了这种 when 方法的库。其他的 promises 库,例如 Q, Dojo, 和 when 依照 Promises/B spec 实现了 when 方法, 但是并没有实现注释者提及的 when 方法。但是,Q 库有一个 all方法,when.js 也有一个 parallel方法,与上面的 jQuery.when 方法作用一样,只是它们接受一个数组类型的参数,而不是任意数量的参数。
值的表示
另一个评论者给我留言:
Promise是处理以下场景的更好的方法:
"我想在这个数据库中找一个用户,但find方法是异步的。"
因此,这里我们有了一个不能立刻返回值的find方法。但最终它确实"返回"了一个数值(通过一个回调的方式),而你希望以某种方式处理那个数值。现在,通过使用一个回调,你能定义一个继续部分,或者说“一些将在以后时间里处理那个数值的代码”
Promise改变了那种“嘿,这里是一些你会发现你用来处理返回数值的代码”。它们是一些允许"find"方法说“嘿,我将忙着找你要找的信息,但与此同时你能继续等着返回结果,而且你能同时以任何你希望的方式处理它,就像实际的东西!”
Promise代表了真实的数值。那就是陷阱。它们工作在你像处理实际东西一样处理Promise的时候。Promise的JavaScript实现期待你给它传递一个回调函数,这只是一个“巧合”,它不是重要的事情。
我相信这真的就是promise的重点。为什么?读一读 Promise/A规范 的第一句“一个promise代表了一个操作的一次完成最终返回的数值。“使它有点明显了,是不是?好吧,即使那就是重点,那也不能阻止我在后面本文中呈现其他人的见解。不管怎么说,我们再多谈论这个思想一点。
这个概念是美好的,但它在实践中是如何体现的?把promises看作数值的表现形式看起来像什么?首先,让我们来看看一些同步的代码:
Synchronous Code
- // Search a collection for a list of "specific" items
- var dataArray = dataCollection.find('somethingSpecific');
- // Map the array so that we can get just the ID's
- var ids = map(dataArray, function(item){
- return item.id;
- });
- // Display them
- display(ids);
好的,如果dataCollection.find是同步的,这段代码能正常工作。但是如果它是异步的呢?我的意思是看看这段代码;它完全是同步方式写的。如果find是异步的,没有方法能保证运行正确,对不?不对。我们仅需修改map和display,接受promises作为参数,代表用作计算的数据。同样,find和map需要返回promises。所以假设dataCollectionque确实不包含数据:仅仅只是调用AJAX获取数据。所以现在将返回一个promise。现在,让我们重写map和display,接受promises,但是我们取不同的名字:pmap和pdisplay。
#p#
接受Promise作为参数
- function pmap (dataPromise, fn) {
- // Always assume `dataPromise` is a promise... cuts down on the code
- // `then` returns a promise, so let's use that instead of creating our own
- return dataPromise.then(
- // Success callback
- function(data) {
- // Fulfill our promise with the data returned from the normal synchronous `map`
- return map(data, fn);
- }
- // Pass errors through by not including a failure callback
- );
- }
- function pdisplay(dataPromise) {
- dataPromise.then(
- // Success callback
- function(data) {
- // When we have the data, just send it to the normal synchronous `display`
- display(data);
- },
- // Failure callback
- function(err) {
- // If it fails, we'll just display the error
- display(err);
- }
- );
- }
这不会太难, 是吗? 让我们现在用这些新函数重新编写上面那个例子:
异步代码
- // Search a collection for a list of "specific" items
- var dataArray = dataCollection.find('somethingSpecific');
- // Map the array so that we can get just the ID's
- var ids = pmap(dataArray, function(item){
- return item.id;
- });
- // Display them
- pdisplay(ids);
我所要做的是修改这些函数的名字。假如你很自信,你完全可以用相同的名字编写这些函数,接受一个promise或者普通值,做出相应的反应。在新代码中,这些promise表示返回的最终值,所以我们能以promise看起来像真实值的方式来编写代码。
我撒了点小谎。上面我说过“这会工作得完全一样”,但这是个棘手的问题。除了一些函数的名字改变了,这里还有一些别的不同:在调用pdisplay之后出现的任何代码,有可能在实际的显示发生之前被调用。所以你要么需要使后面的代码不依赖于显示的结束,要么需要从pdisplay返回一个promise,并且使其他的代码在promise被装载之后运行。在我的例子中没有使pdisplay返回一个promise的一部分原因是,它没有返回的数值,因此在我们讨论本节中,promise不能被用来表示成一个数值。
不管怎样,你可以看到如何使你的函数接受promise,而不只是普通的数值,可以使你的代码看起来更干净更靠近像同步代码一样工作。那是promise的一个美妙之处。在Jedi Toolkit的博客上,从一个略微不同的观点,有另一个关于这个概念的帖子。
为了流畅的API内部使用
某人评论我的文章说:
我想在解释promise是做什么的方面,我们写promise实现的那些人做得很糟糕。我的观点是,你作为一个用户永远都不应该被迫与promise用then()交互。then()是promise消费库用来相互之间交互,并且提供流畅的API的一种途径。你应该仍然像通常一样使用回调与事件。 |
他的评论非常好的契合了前面关于使用promise代表数值的一节。通过使用promise代表数值,我们能够创建简单的像上面看到的API,但这里我们在讨论一个链接的API。根本上说,这个评论者是在告诉我们,要在一个异步命令链的末尾使用回调,以便我们仍然在做我们过去习惯做的(在末尾使用回调),而无人能说出我们在使用promise。他想使promise远离普通用户,只在我们自己库的内部使用。看一下这个查询数据库,并越来越异步过滤结果的例子。
Chained .then Calls
- database.find()
- .then(function(results1) {
- return results1.filter('name', 'Joe');
- })
- .then(function(results2) {
- return results2.limit(5);
- })
- .then(function(resultsForReal) {
- // Do stuff with the results
- });
不论什么原因,filter和limit其实是异步的。 可能你觉得它们不应该这样,但是它们就是这样的。好了,评论家建议修改 API,保证用户可以这样使用:
顺畅的API例子
- database.find().filter('name', 'Joe').limit(5, function(results){
- // Do stuff with the results
- });
这对我似乎更有意思。如果你能掌控它的运行,这是你应当采取的线路。你可以修改一点, 替代普通的回调,仍然返回promise:
返回Promise
- var results = database.find().filter('name', 'Joe').limit(5);
- results.then(function(realResults){
- // Do stuff with the results
- });
- // OR use results like in the previous section:
- doStuffWith(results);
选择权在你。我认为老道的开发者明白返回promise给用户没有什么问题,但这确实是个见仁见智的问题。无论哪种方式,都比我们需要先串起再调用的情况好许多。
同步并行的错误隔离
有一篇相当著名的文章叫做You're Missing the Point of Promises,和本文观点一致。那篇文章中,Domenic Denicola(押头韵的好名字)指导了部分关于then函数的Promises/A规范。
then(fulfilledHandler, errorHandler, progressHandler) 添加了fulfilledHandler、errorHandler和progressHandler,这三个处理函数在promise完成时被调用。Promise完成时,调用fulfilledHandler。Promise失败时,调用errorHandler。触发postgress事件时,调用progressHandler。所有参数都是可选的,并且非函数类型的值会被忽略。不仅progressHandler参数是可选的,而且progress也是完全可选的。Promise的实现者们不必要在任何时候调用progressHandler,如果有postgress事件到来就调用它。 给出的fulfilledHandler和errorHandler回调函数完成时,此函数应当返回一个成功的新promise。这就允许promise操作串起来。回调函数handler的返回值是返回promise的完成值。如果回调函数抛出错误,那么返回的promise会被移交到失败状态。 |
在他的文章中,他在最后一段表明了用途,这是他称之为的“最重要”的一段。他说:
问题是,promises不是回调函数的聚集体。那是一种简单的功用。Promises是更加深层的东西,具体说就是,它提供了同步和异步函数的直接对应。
我完全同意这个观点。然后他继续特别地关注最后的那个句子:“如果回调函数抛出一个错误,返回的promise将会转为失败状态。”他关注这个句子的原因是jQuery的实现可能实现了Promises/A规范,但没有这样做。如果一个错误在回调函数里抛出,它在promise的实现里变得不可捕获。
对于大多数人来说这点是非常重要的,尽管我还没有遇到这种情况,这种情况确实是个问题,因为我不经常抛出错误。错误的promise等价于一个错误或者异常,所以,如果错误存在,这时应该是失败而不是抛出一个错误。这种方法下我们能够继续使用promise来并行同步操作。我认为我们都应该 把这个bug报告给jQuery。更多的人去报告这错误,就更可能快的被修复。jQuery的promise是最常使用的实现之一,所以我们应该确定他们的做法是正确的。
结论
啊呀!这肯定是我写过的最长的帖子了。我原想只是这么一半长的!不管怎样,promise的重点是它代表一个操作返回的最终结果值,但使用它们的原因是使同步操作更好的并行。自从异步编程进入此场景,到处都是弹出的回调,以奇怪的方式遮住我们的代码。Promise是一种改变其的方法。Promise允许我们以同步的方式写代码,同时给予我们代码的异步执行。
原文链接:http://www.oschina.net/translate/what-is-the-point-of-promises