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 描述了一种定义模块的机制,模块及其依赖模块可以通过这种机制进行加载.该机制特别适用于浏览器. 本规范曾 ...
随机推荐
- App- 借书趣
借书趣是一款方便用户在上海图书馆借书助手应用.通过扫描条码,导入豆瓣想读等手段可以方便管理想读想借的书目应用通过上图的接口和一些算法帮助用户生成借书单,规划用户可以去上图的哪个分馆可以借到最多想要阅读 ...
- 摄像头拍照,PHP输入流php://input的使用分析
在做一个摄像头拍照然后上传的功能,php中使用php://input来获取内容.于是就了解了下php://input. 从官网信息来看,php://input是一个只读信息流,当请求方式是post的, ...
- PHP判断文件夹是否存在和创建文件夹的方法(递归创建多级目录)
在开始之前,我先说明一下,可能许多朋友与我一样认为只要给一个路径,mkdir就可以创建文件夹,其实不是那样,单个的MKDIR只能创建一级目录,对于多级的就不行了,那如何用mkdir来创建呢?先我抄一段 ...
- class Solution(object): def fizzBuzz(self, n): a = [] i = 1 while(i <= n): if(i%15 == 0): a.append("FizzBuzz") elifleetcode day_01
412. Fizz Buzz Write a program that outputs the string representation of numbers from 1 to n. But fo ...
- JQuery实现图片轮播效果源码
======================整体结构======================== <div class="banner"> <ul class ...
- Android 设置启动界面
启动界面的意义是为了让后台处理耗时的复杂工作,当工作处理完成后,即可进入主界面.相比让用户等待布局加载完成,使用一张图片作为启动背景,会带来更好的体验. 首先,需要建立一个简单的布局: <?xm ...
- getRealPath("/")弃用
用request.getSession().getServletContext().getRealPath("/");替换getRealPath("/"):
- swift 001
swift 001 = 赋值是没有返回值的 所以 int a=10; int b=20; if(a=b){ printf("这个是错误的"); } swift 中的模运算 是支 ...
- .NET 多线程
多线程 在一个程序中,这些独立运行的程序片断叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”.多线程处理一个常见的例子就是用户界面.利用线程,用户可按下一个按钮,然后程序会立即作出响 ...
- cell 的复用机制
一个问题引发的血案,以下是本侦探的探案过程的一部分:以下全部都是转载自别人的博客:http://blog.sina.com.cn/s/blog_9c3c519b01016aqu.html 转自:htt ...