`

你没有抓住Promises的要点

阅读更多

注:这篇文章翻译自《You're Missing the Point of Promises》,阅读这篇文章,你首先需要对于JavaScript中的Promises是什么有了解,否则,你可以先看一看这篇文章(英文),或者这篇文章(中文)掌握基础。有一些修改,另受水平所限,翻译的不当之处请参阅原文。

Promises是一种令代码异步行为更加优雅的抽象。如果用最基本的编码方式,代码是这种连续的形式:

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

现在这样的方法返回一个被称作promise的值,它表示的是一个操作的最终执行结果。

var promiseForTweets = getTweetsFor("domenic");

这个就很有用了,因为你可以把promise当做一等公民来对待了:传值给他,聚合对它们的调用等等,而不是搞一堆耦合在一起的回调函数来完成你的逻辑。

我已经讲过了promises有多酷,所以我现在不说这个了,我现在要说的是一个现今JavaScript库中非常令人不安的趋势:声称支持promise,却根本没有抓住它的要点。

 

Then方法和CommonJS的Promises/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来处理异常:

try {
  process.nextTick(function () {
    // do something
  });
} catch (err) {
  //you can not catch it
}

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

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才能处理它。

换言之,下面这段代码:

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);
    }
  );

相当于这样的同步代码:

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”对象:

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对象:

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

文章系本人原创,转载请保持完整性并注明出自《四火的唠叨》

4
0
分享到:
评论

相关推荐

    Node.js-make-promises-safe一个让promises的使用免于内存泄漏的node.js模块

    1. 首先,你需要安装`make-promises-safe`模块,通过npm可以轻松完成: ```bash npm install make-promises-safe ``` 2. 在你的项目入口文件(如`index.js`)中,引入并启动`make-promises-safe`: ```...

    Understanding JavaScript Promises 理解JavaScript Promises

    Which parts of promises are synchronous and which are asynchronous How to effectively use promise chains to simplify your code The best way to catch errors in promises How to create your own promises ...

    rxjava-promises, 保证JVM语言的实现.zip

    rxjava-promises, 保证JVM语言的实现 rxjava承诺保证基于JVM的语言库重要说明不幸的是,这个库是在RxJava不成熟的阶段建立的,我在 操作系统的经验非常差。你还在维护这个?不,不过,许多雇主仍然要求你的GitHub...

    PHP 5.5 yield and promises

    Coroutines generalize subroutines to allow multiple entry points for suspending and resuming execution at certain locations.cooperative tasks, exceptions, event loop,iterators, infinite lists and ...

    Thinking.in.Promises.2015.6.pdf

    《Thinking in Promises》这本书,由Mark Burgess撰写,探讨了Promise Theory(承诺理论),这是一种设计系统以促进合作的理论框架。Promise Theory的核心观点是,系统可以被理解为由不同部分组成,每个部分从自己的...

    Mastering JavaScript Promises(PACKT,2015)

    To overcome these limitations, a concept called JavaScript promises is rising rapidly in popularity. Promises makes writing complex logics more manageable and easy. This book starts with an ...

    swift-Promises一个为Objective-C和Swift提供同步构造的现代框架

    在Google Promises库(google-promises-47a49da)中,你可以找到这个Promise实现的具体源代码和示例。这个库不仅适用于Swift,还兼容Objective-C,因此对于跨语言项目具有很好的兼容性。 总的来说,Swift Promises...

    JS返回Promises对象插件Allora.zip

    Allora可以使对象中的方法执行后返回Promise对象,主要用在一些异步调用的方法(onload/setTimeout/requestAnimationFrame等)中。使用示例:BasicOr simply:const myWindow = allora(window) ...

    详解Javacript和AngularJS中的Promises

    在JavaScript和AngularJS开发中,处理异步任务是一项至关重要的能力。Promise是JavaScript中处理异步编程的一个核心概念,它允许我们以更优雅的方式编写和管理异步代码。AngularJS作为基于JavaScript的前端框架,...

    前端开源库-batch-promises

    "batch-promises" 就是为了解决这一问题而诞生的前端开源库,它专注于批量承诺,使得批量处理异步任务变得更加简单和高效。 批量承诺(Batch Promises)的概念是指将一组独立的 Promise 操作合并为一个大批次,然后...

    前端开源库-async-promises

    在前端开发中,异步处理是至关重要的,尤其是在JavaScript这种单线程环境中。"async-promises"是一个专门针对异步操作的开源库...如果你在项目中遇到异步控制流的挑战,`async-promises`绝对是一个值得尝试的解决方案。

    fastly-promises-源码.rar

    《深入剖析Fastly-Promises源码》 在现代JavaScript开发中,Promise是异步编程的核心工具,它为我们处理复杂的回调地狱提供了优雅的解决方案。Fastly-Promises是一款高效的Promise库,其源码包含了Promise的实现...

    Promises/A+PHP库,支持同步_

    在"promises-master"这个压缩包中,你可能找到以下内容: - `src`目录:包含Promise类和其他相关组件的源码。 - `tests`目录:可能包含了对库的各种测试用例,通过这些测试可以了解库的使用方法和预期行为。 - `...

    基于PromisesA规范的简单实现

    Promises 是 JavaScript 中用于异步编程的重要工具,它允许我们以一种更加有序、易于理解和管理的方式处理复杂的...Brant-Ma-promising-f6a31b2 这个代码仓库可能包含了更完整的实现,你可以查看源码以获取更多细节。

    可组合 Promises 和 Promises 作为组件.zip

    vue-promised 用风格来履行你的承诺帮助我以可持续的方式继续致力于开源 。每月只需 1 美元,即可在 Github 上赞助我。银牌赞助商 铜牌赞助商安装npm install vue-promised# oryarn add vue-promised如果你使用 Vue ...

    如何利用Promises编写更优雅的JavaScript代码

    你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别。难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它们写出更优雅的 ...

    Promises的JavaScript实现promisejs.zip

    promise.js 是 Promises 的轻量级 JavaScript 实现。 Promises 提供了 callback-passing 的替代方案,异步函数返回一个 Promise 对象可附加到 callbacks 中。 示例代码: function asyncfoo() { var p = new ...

Global site tag (gtag.js) - Google Analytics