CommonJS Promises/A规范
本文来自四火哥的翻译
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
|
// aaaah, much better |
CommonJS Promises/A规范的更多相关文章
- JS魔法堂:剖析源码理解Promises/A规范
一.前言 Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现 如Q, Bluebird, when, rsvp. ...
- JavaScript中Promises/A+规范的实现
Promises是一种异步编程模型,通过一组API来规范化异步操作,这样也能够让异步操作的流程控制更加容易. 下面的代码是假设执行一个异步队列,每一项都会使用上一项返回的数据: function ne ...
- 前端翻译:Promises/A+规范
原文地址:https://promisesaplus.com/ 本篇为原文翻译+个人理解,若有谬误请各位指正,谢谢. 尊重原创,转载请注明来自:http://www.cnblogs.com/fsjoh ...
- Javascript模块化编程系列三: CommonJS & AMD 模块化规范描述
CommonJS Module 规范 CommonJS 的模块化规范描述在Modules/1.1.1 中 目前实现此规格的包有: Yabble,CouchDB,Narwhal (0.2), Wakan ...
- 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise
本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...
- 对 Promises/A+ 规范的研究 ------引用
作为 Modern JavaScript 基础设施的一部分,Promises 对前端开发者而言异常重要.它是 async/await 语法的基础,是 JavaScript 中处理异步的标准形式.并且, ...
- 【翻译】Promises/A+规范
目录 介绍 译文 1. 术语(Terminology) 2. 要求(Requirements) 2.1 Promise状态 2.2 then方法 2.3 Promise解析程序 3. 注释 3.1 p ...
- Promises/A+规范
为什么需要异步编程方式 一个函数执行之后,在它后面顺序编写的代码中,如果能够直接使用它的返回结果或者它修改之后的引用参数,那么我们通常认为该函数是同步的. 如果一个函数的执行结果或者其修改的引用参数, ...
- CommonJS 的 AMD 规范
异步模块定义(Asynchronous Module Definition,简称 AMD)API 描述了一种定义模块的机制,模块及其依赖模块可以通过这种机制进行加载.该机制特别适用于浏览器. 本规范曾 ...
随机推荐
- 在Activity之间传递参数(一)
准备: 一.创建主界面:activity_main.xml文件中<Button android:text="启动另一个Activity" android:id="@ ...
- 【转载】使用Pandas对数据进行筛选和排序
使用Pandas对数据进行筛选和排序 本文转载自:蓝鲸的网站分析笔记 原文链接:使用Pandas对数据进行筛选和排序 目录: sort() 对单列数据进行排序 对多列数据进行排序 获取金额最小前10项 ...
- R笔记 单样本t检验 功效分析
R data analysis examples 功效分析 power analysis for one-sample t-test单样本t检验 例1.一批电灯泡,标准寿命850小时,标准偏差50,4 ...
- 【总结1】PhpStorm配置XDebug(远程)调试PHP
配置PhpStorm调试PHP 第一步:配置 XDebug 下载安装XDebug到本地环境,打开php.ini,参考配置如下: [XDebug] zend_extension = "D:\P ...
- Linux下安装和配置JDK与Tomcat(升级版)
在这个版本 Linux下安装和配置JDK与Tomcat(入门版) 的基础上优化升级 1.下载相关软件 apache-tomcat-6.0.37.tar.gz jdk-6u25-linux-i586-r ...
- ubuntu和centos安装RRDTool——cacti前置技能
Installing Pre-Requisites Note that RRDTool 1.0.x versions included all dependancies, but 1.2.x vers ...
- java27
1:反射(理解) (1)类的加载及类加载器 (2)反射: 通过字节码文件对象,去使用成员变量,构造方法,成员方法 (3)反射的使用 A:通过反射获取构造方 ...
- ABAP 订单转交货单
*& Report ZSDR025 *& *&---------------------------------------------------------------- ...
- ubuntu下安装nodejs,无node情况
Updating nodejs solved the issue: npm cache clean -f npm install -g n n stable node --version node ...
- <%#Eval if判断用法
1.绑定Repeater 基础用法 <%#Eval("RoleID")%> 2.简单判断用法 <td> <%# Convert.ToBoolean(E ...