本文来自四火哥的翻译

CommonJS是一组javascript编程规范,而promise是其中之一。

简而言之,promises是一种令代码的异步行为变得更加优雅的软件抽象。在基本的定义中,代码可能一直是这样写的

getTweetsFor("domenic", function (err, results) {
// the rest of your code goes here.
});

现在你的方法有一个返回值,叫做promise,它代表操作的最终结果。

var promiseForTweets = getTweets("domenic");

这是很重要的因为你可以把promiseForTweets当作一等对象,传值给他们,聚合他们等等,而不是搞一堆耦合在一起的回调函数去完成你逻辑。

我曾经说过我认为promises有多酷,这里就不再赘述。相反,我现在要说的是我看到一个不安的趋势关于最近javascript类库已经加入promise支持,他们完全忽略了promise的关键点。


Then方法和CommonJS Promise/A规范

如果有人说promise是JavaScript的上下文,那么他至少指的是CommonJS的Promises/A规范。这大概是我见过的最简陋的规范了,基本上只是对于这一类函数的行为做了简单说明:

promise是一种以函数来作为then属性值的对象:

then(fulfilledHandler, errorHandler, progressHandler)

添加fulfilledHandler、errorHandler和progressHandler后,promise对象就构成了。fulfilledHandler是在promise被装载数据的时候调用,errorHandler在promise失败的时候调用,progressHandler则在progress事件触发的时候调用。所有的参数都是可选的,并且非function的参数都会被忽略掉。有时progressHandler并不只是一个可选参数,但是progress事件确是纯粹的可选参数而已。promise模式的实现者并不一定要每次都调用progressHandler(因为它可以被忽略掉),只有这个参数传入的时候才会发生调用。

这个方法在fulfilledHandler或者errorHandler回调完成之后,得返回一个新的promise对象。这样一来,promise操作就可以形成链式调用。回调handler的返回值是一个promise对象。如果回调抛出异常,这个返回的promise对象就会把状态设为失败。

人们一般都理解第一段话,基本上可以归结为回调函数的聚合。

通过then方法来关联起回调函数和promise对象,不管是成功、失败还是进行中。当promise对象改变状态时(这超出了这篇短小文档讨论的范围),回调函数会被执行,我觉得这很有用。

但是人们不怎么理解的第二段,恰恰是最重要的。


那么Promises的要点是啥?

最重要的是,promises根本就不是简单的回调函数聚合。promises并不是那么简单的东西,它是一种为同步函数和异步函数提供直接一致性的模式。

啥意思呢?我们先来看同步函数两个非常重要的特性:

  • 它们都有返回值
  • 它们都可以有异常抛出

这两个都是必不可少的。你可以把一个函数的返回值作为参数传给下一个函数,再把下一个函数的返回值作为参数传给下下个,一直重复下去。现在,如果中间出现失败的情况,那个函数的链会抛出异常,异常会向上传播,直到有人可以来处理它为止。

在异步编程的世界里,你没法“返回”一个值了,它没法被及时地读取到。相似的,你也没法抛出异常了,因为没有人回去捕获它。所以我们踏入了“回调的地狱”,返回值嵌套了回调,错误需要手动传给原有的调用链,这样你就得引入类似于像domain这样疯狂的东西了。

下面四火对domain做一个小的说明:

异步编程中,你没法简单地通过try-catch来处理异常:

1
2
3
4
5
6
7
try {
  process.nextTick(function () {
    // do something
  });
} catch (err) {
  //you can not catch it
}

所以Node.js给的使用domain的解决方法是:

1
2
3
4
5
var doo = domain.create();
// listen to error event
doo.on('error', function (err) {
  // you got an error
});

当然,这个方法并不完美,还是会存在堆栈丢失等问题。

promises现在需要给我们异步世界里的函数组成和错误冒泡机制。现在假使你的函数要返回一个promise对象,它包含两种情况:

  • 被某个数据装载(fulfill)
  • 被某个异常的抛出中断了

如果你正确遵照Promises/A规范实现,fulfillment或者rejection部分的代码就像同步代码的副本一样,在整个调用链中,fulfillment部分会执行,也会在某个时候被rejection中断,但是只有预先声明了的handler才能处理它。

换言之,下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getTweetsFor("domenic") // promise-returning function
  .then(function (tweets) {
    var shortUrls = parseTweetsForUrls(tweets);
    var mostRecentShortUrl = shortUrls[0];
    return expandUrlUsingTwitterApi(mostRecentShortUrl); // promise-returning function
  })
  .then(httpGet) // promise-returning function
  .then(
    function (responseBody) {
      console.log("Most recent link text:", responseBody);
    },
    function (error) {
      console.error("Error with the twitterverse:", error);
    }
  );

相当于这样的同步代码:

1
2
3
4
5
6
7
8
9
try {
  var tweets = getTweetsFor("domenic"); // blocking
  var shortUrls = parseTweetsForUrls(tweets);
  var mostRecentShortUrl = shortUrls[0];
  var responseBody = httpGet(expandUrlUsingTwitterApi(mostRecentShortUrl)); // blocking x 2
  console.log("Most recent link text:", responseBody);
} catch (error) {
  console.error("Error with the twitterverse: ", error);
}

不管错误怎样发生,都必须要有显式的错误捕获处理机制。在将要到来的ECMAScript 6的版本中,使用了一些内部技巧,大多数情况下代码还是一样的。

第二段话

第二段话其实是完全有必要的:

这个方法在fulfilledHandler或者errorHandler回调完成之后,得返回一个新的promise对象。这样一来,promise操 作就可以形成链式调用。回调handler的返回值是一个promise对象。如果回调抛出异常,这个返回的promise对象就会把状态设为失败。

换言之,then方法并没有一个机制去把一堆回调方法附着到某个集合中去,它的机制只不过是把原有对象转换成promise对象,以及生成新的promise对象。

这就解释了第一段的关键:函数应当返回一个新的promise对象。JQuery(1.8以前的版本)却不这么做。他们只是继续使用原有的promise对象,但是把它的状态改变一下而已。这就意味着如果你把promise对象给客户了,他们其实是可以可以改变它的状态的。为了说明这一点有多荒谬,你可以想一想一个同步的例子:如果你把一个函数的返回值给了两个人,其中一个可以改变一下返回值里面的东西,然后这两个人手里的返回值居然就抛出异常来了!事实上,Promises/A规范其实已经说明了这一点:

一旦promise装载数据完成或者失败了,promise的值就不可以再改变了,就像JavaScript中的数值、原语类型、对象ID等等,都是不可以被改变的。

现在考虑其中的最后两句话,它们说出了promise是怎样被创建的:

  • 如果handler返回了一个值,那么新的promise就要装载那个值。
  • 如果handler抛出异常,那么新的promise就要用一个异常来表示拒绝继续往后执行。

我们根据promise的不同状态把这个场景分解一下,就可以知道为什么这几句话那么重要了:

  • 数据装填完成,fulfillment handler返回了一个值值:简单的函数转换
  • 数据装填完成,但是fulfillment handler抛出了异常:获取数据,然后再抛出异常
  • 数据装填失败,rejection handler返回了一个值:必须得用一个catch子句捕获异常并处理
  • 数据装填失败,但是rejection handler抛出了异常:必须得用一个catch子句捕获并重新抛出(可以重新抛出一个新的异常)

如果没有这些,你就失去了同步/异步并行处理的威力,那么你的所谓的“promises”也就变成了简单的回调函数聚合而已了。这也是JQuery当前对promises的实现的问题所在,它只实现了上面说的第一个场景而已。这也是Node.js 0.1中基于EventEmitter的promise的问题之一。

更进一步说,捕获异常并转换状态,我们需要处理预期和非预期的异常,这和写同步代码没什么区别。如果你在某个handler里面写一个叫做aFunctionThatDoesNotExist()的函数,你的promise对象失败以后会抛出异常,接着你的异常向上冒泡,外面最近的一个rejection handler会处理它,这看起来就像你在那里手写了new Error("bad data")一样。看吧,没有domain。

那又如何

也许你现在被我这样一波一波的解释感到压力陡增,想不明白为什么我会对那些写出这些糟糕行为的类库那么恼火。

现在我告诉你为什么:

promise对象是一个被定义为拥有一个then方法的返回值的对象。

对于Promises/A规范实现类库的作者,我们必须做到:凡是写出then方法这样机制的promise,都得去完全地符合Promises/A规范。

如果你也认为这样的话是对的,那么你也可以写出这样的扩展库,不管是Q、when.js,或者是WinJS,你可以使用Promises/A规范中最基本的规则定义,去构建promise的行为。比如这个,一个可以和一切真正满足Promises/A规范的类库一起工作的retry函数。

然而,不幸的是,像JQuery这样的类库却破坏了这条守则,它迫使丑陋的hack代码去检测这些冒充promises的对象——虽然JQuery依然在API文档里面号称这是“promise”对象:

1
2
3
if (typeof assertion._obj.pipe === "function") {
    throw new TypeError("Chai as Promised is incompatible with jQuery's so-called “promises.” Sorry!");
}

如果API的使用者坚持使用JQuery promises的话,你大概只有两种选择:在执行过程中莫名其妙地、令人困惑地失败,或者彻底失败,并且阻塞你继续使用整个类库。这可真糟糕啊。

继续向前

这就是我为什么尽可能地避免在Ember中使用回调函数聚合器了,这也是我写这篇文章的原因,而且,你可以看一下我写的这个准确兼容Promises/A规范的套件,这样我们就可以在认识层面上达成一致了。

这个测试套件发布以后,promise操作性和可理解性都有了进步。rsvp.js发布的其中一个目标就是要提供对Promises/A的支持。不过最棒的是这个Promises/A+组织的开源项目,一个松耦合的实现,用清晰的和测试完备的方式呈现扩展了原有Promises/A规范,成为Promises/A+规范

当然,还有很多工作要做。值得注意的是,在写这篇文章的时候,JQuery的最新版本是1.9.1,它的promises在错误处理上的实现是完全错误的。我希望在接下去的JQuery 2.0版本中参考Promises/A+的文档,修正这个问题。

同时,这些类库是非常好地遵照Promises/A+标准的,我现在毫无保留地推荐给你:

  • Q:Kris Kowal和我写的,一个promise特性完全实现的类库,有丰富的API、Node.js的支持、处理流支持,以及初步的对于长堆栈的支持。
  • RSVP.js:Yehuda Katz写的,非常轻量的promise的完全实现。
  • when.js:Brian Cavalier写的,一个任务管理的中间库,可以部署和取消任务执行。

如果你对使用JQuery残废的promise感到不爽,我推荐你使用上面类库的工具方法来实现你同样的目的(一般都是一个叫做when的方法),把这个残废的promise对象变成一个健全的promise对象:

1
2
var promise = Q.when($.get("https://github.com/kriskowal/q"));
// aaaah, much better

CommonJS Promises/A规范的更多相关文章

  1. JS魔法堂:剖析源码理解Promises/A规范

    一.前言 Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现 如Q, Bluebird, when, rsvp. ...

  2. JavaScript中Promises/A+规范的实现

    Promises是一种异步编程模型,通过一组API来规范化异步操作,这样也能够让异步操作的流程控制更加容易. 下面的代码是假设执行一个异步队列,每一项都会使用上一项返回的数据: function ne ...

  3. 前端翻译:Promises/A+规范

    原文地址:https://promisesaplus.com/ 本篇为原文翻译+个人理解,若有谬误请各位指正,谢谢. 尊重原创,转载请注明来自:http://www.cnblogs.com/fsjoh ...

  4. Javascript模块化编程系列三: CommonJS & AMD 模块化规范描述

    CommonJS Module 规范 CommonJS 的模块化规范描述在Modules/1.1.1 中 目前实现此规格的包有: Yabble,CouchDB,Narwhal (0.2), Wakan ...

  5. 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise

    本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...

  6. 对 Promises/A+ 规范的研究 ------引用

    作为 Modern JavaScript 基础设施的一部分,Promises 对前端开发者而言异常重要.它是 async/await 语法的基础,是 JavaScript 中处理异步的标准形式.并且, ...

  7. 【翻译】Promises/A+规范

    目录 介绍 译文 1. 术语(Terminology) 2. 要求(Requirements) 2.1 Promise状态 2.2 then方法 2.3 Promise解析程序 3. 注释 3.1 p ...

  8. Promises/A+规范

    为什么需要异步编程方式 一个函数执行之后,在它后面顺序编写的代码中,如果能够直接使用它的返回结果或者它修改之后的引用参数,那么我们通常认为该函数是同步的. 如果一个函数的执行结果或者其修改的引用参数, ...

  9. CommonJS 的 AMD 规范

    异步模块定义(Asynchronous Module Definition,简称 AMD)API 描述了一种定义模块的机制,模块及其依赖模块可以通过这种机制进行加载.该机制特别适用于浏览器. 本规范曾 ...

随机推荐

  1. 在Activity之间传递参数(一)

    准备: 一.创建主界面:activity_main.xml文件中<Button android:text="启动另一个Activity" android:id="@ ...

  2. 【转载】使用Pandas对数据进行筛选和排序

    使用Pandas对数据进行筛选和排序 本文转载自:蓝鲸的网站分析笔记 原文链接:使用Pandas对数据进行筛选和排序 目录: sort() 对单列数据进行排序 对多列数据进行排序 获取金额最小前10项 ...

  3. R笔记 单样本t检验 功效分析

    R data analysis examples 功效分析 power analysis for one-sample t-test单样本t检验 例1.一批电灯泡,标准寿命850小时,标准偏差50,4 ...

  4. 【总结1】PhpStorm配置XDebug(远程)调试PHP

    配置PhpStorm调试PHP 第一步:配置 XDebug 下载安装XDebug到本地环境,打开php.ini,参考配置如下: [XDebug] zend_extension = "D:\P ...

  5. Linux下安装和配置JDK与Tomcat(升级版)

    在这个版本 Linux下安装和配置JDK与Tomcat(入门版) 的基础上优化升级 1.下载相关软件 apache-tomcat-6.0.37.tar.gz jdk-6u25-linux-i586-r ...

  6. ubuntu和centos安装RRDTool——cacti前置技能

    Installing Pre-Requisites Note that RRDTool 1.0.x versions included all dependancies, but 1.2.x vers ...

  7. java27

    1:反射(理解)    (1)类的加载及类加载器    (2)反射:        通过字节码文件对象,去使用成员变量,构造方法,成员方法    (3)反射的使用        A:通过反射获取构造方 ...

  8. ABAP 订单转交货单

    *& Report  ZSDR025 *& *&---------------------------------------------------------------- ...

  9. ubuntu下安装nodejs,无node情况

      Updating nodejs solved the issue: npm cache clean -f npm install -g n n stable node --version node ...

  10. <%#Eval if判断用法

    1.绑定Repeater 基础用法 <%#Eval("RoleID")%> 2.简单判断用法 <td> <%# Convert.ToBoolean(E ...