27 August 2013

凌乱的异步编程 一文中,当为一组简单的函数调用引入回调函数时,我们看到的是一副尴尬的局面,即使是用这种方式来处理单个异步操作。

快速回顾一下,看看我们最初的代码、使用回调函数时的凌乱结果,以及我们为了回到正途而想要解决的几个问题:

  1. 我们再也不能使用简单的调用-返回编程模型。
  2. 我们再也不能使用 try/catch/finally 来处理异常。
  3. 我们必须为可能执行异步操作的每个函数的签名增加 callback 和 errback 参数。

Promises

一个 Promise(又名 Future, Delayed value, Deferred value)代表一个尚不可用的值,因为产生这个值的计算过程尚未完成。一个 Promise 是最终的成功结果或失败原因的占位符。

在《jQuery技术内幕》一书中,把 Deferred 称为“异步队列”,把 Promise 称为“异步队列的只读副本”。

Promises 还提供了一个简单的 API(见下文),用于在结果完成或故障发生时获取通知。

Promises 不是一个新概念,已经在许多语言中被实现。在 JavaScript 中,Promise 概念的实现已经有一段时间了,并且最近变得更加流行,因为我们开始构建更庞大、更复杂的系统,需要协调更多的异步任务。

(注意:虽然 Promise API 标准存在多个提案,但是 Promises/A+ 已经在多个主流框架中被实现,似乎正在成为事实上的标准。无论哪种提案,基本的概念是相同的:1) Promises 作为结果或错误的占位符,2) 提供了一种在结果完成或错误发生时的通知方式。)

典型的 XHR 示例

在 XHR Get 示例中,我们关注的是获取 url 的内容。我们知道 XHR 是一个异步操作,值不会立即可用。这种情况完全符合一个 Promise 的定义。

假设我们有一个 XHR 库,它立即返回一个 Promise 作为内容的占位符,而不是要求我们传入一个回调函数。我们可以重写 Part 1 中的异步函数 thisMightFail,就像这样:

function thisMightFail() {
    // Our XHR library returns a promise placeholder for the
    // content of the url.  The XHR itself will execute later.
    var promise = xhrGet('/result');

    // We can simply return the promise to our caller as if
    // it is the actual value.
    return promise;
}

(需要注意的是,一些流行的 JavaScript 库,包括 @bryanforbesDojo(请参考 great article on Dojo's Deferred) 和 jQuery,使用了 Promises 来实现 XHR 操作)

现在,我们可以返回 Promise 占位符,就像它是真正的结果,并且异步函数 thisMightFail 看起来非常像传统的同步操作和调用-返回操作。

调用栈

在一个没有回调函数的世界里,结果和错误沿着调用栈向上回传。这是一种符合预期和友好的模式。而在一个基于回调函数的世界里,正如我们已经看到的,结果和错误不再遵循这种熟悉的模式,回调函数必须*向下传递,深入调用栈。

通过使用 Promises,我们可以恢复到熟悉的调用-返回编程模型,并移除回调函数。

恢复调用-返回编程模型

为了看看它是如何工作的,让我们从 Part 1 简化版本的同步函数 getTheResult 开始,不使用 try/catch,这样异常将总是沿着调用栈向上传播。

function thisMightFail() {
    //...
    if(badThingsHappened) {
        throw new Error(...);
    }

    return theGoodResult;
}

function getTheResult() {
    // Return the result of thisMightFail, or let the exception
    // propagate.
    return thisMightFail();
}

现在,让我们为上面的代码引入异步的 thisMightFail,它使用了基于 Promise 的 XHR 库。

function thisMightFail() {
    // Our XHR library returns a promise placeholder for the
    // content of the url.  The XHR itself will execute later.
    var promise = xhrGet('/result');

    // We can simply return the promise to our caller as if
    // it is the actual value.
    return promise;
}

function getTheResult() {
    // Return the result of thisMightFail, which will be a Promise
    // representing a future value or failure
    return thisMightFail();
}

使用 Promises 时,getTheResult() 在同步和异步情况下是相同的!并且在这两种情况下,成功结果或失败将沿着调用栈传播到调用者。

移除回调函数

还要注意到的是,没有向调用栈传入回调函数或错误回调函数(或总是执行的回调函数!),也没有污染任何函数的签名。通过使用 Promises,现在我们的函数的外观和行为就像友好的、同步的调用-返回编程模型。

完成了吗?

我们已经使用 Promises 重构了简单的 getTheResult 函数,并且解决了在 Part 1 提出的的两个问题。我们已经:

  1. 恢复了调用-返回编程模型
  2. 移除了参数 callback/errback/alwaysback 的传播

但是,这对 getTheResult 的调用者意味着什么呢?别忘了,我们返回的是一个 Promise,并且,无论成功结果(XHR 的结果)或错误最终将落实到占位符 Promise,到那时,调用者将需要采取一些行动。

对调用者如何?

正如上面所提到的,Promises 提供了一个 API,用于在结果或错误可用时获取通知。例如,在 Promises/A 规范提案中,一个 Promise 含有一个 .then() 方法,而且许多 Promise 库提供了一个 when() 函数来实现同样的目标。

首先,让我们看看在使用基于回调函数的方式时,调用代码可能是什么样子:

// Callback-based getTheResult
getTheResult(
    function(theResult) {
        // theResult will be the XHR reponse content
        resultNode.innerHTML = theResult;
    },
    function(error) {
        // error will be an indication of why the XHR failed, whatever
        // the XHR lib chooses to supply.  For example, it could be
        // an Error.
        errorNode.innerHTML = error.message;
    }
);

现在,让我们看看调用者如何通过 Promises/A API .then() 使用 getTheResult 所返回的 Promise。

// Call promise-based getTheResult and get back a Promise
var promise = getTheResult();

promise.then(
    function(theResult) {
        // theResult will be the XHR reponse content
        resultNode.innerHTML = theResult;
    },
    function(error) {
        // error will be an indication of why the XHR failed, whatever
        // the XHR lib chooses to supply.  For example, it could be
        // an Error.
        errorNode.innerHTML = error.message;
    }
);

或者,更紧凑一些:

getTheResult().then(
    function(theResult) {
        // theResult will be the XHR reponse content
        resultNode.innerHTML = theResult;
    },
    function(error) {
        // error will be an indication of why the XHR failed, whatever
        // the XHR lib chooses to supply.  For example, it could be
        // an Error.
        errorNode.innerHTML = error.message;
    }
);

(Image from The Meta Picture)

这就是 Promises 用来避免使用回调函数的全部内容?我们就这么使用它们?!?

别着急

在 JavaScript 中,通过使用回调函数来实现 Promises,因为没有语言级的结构可以用于处理异步。回调函数是 Promises 必然的实现方式。如果 Javascript 已经提供或者未来可能提供其他的语言结构,那么 Promises 可能会以不同的方式实现。

然而,使用 Promises 解决(Part 1 中)模块之间传递深度回调函数的问题具备几个重要的优点。

首先,我们的函数签名是正常的。我们不再需要为从调用者到 XHR 库的每个函数签名添加 callback 和 errback 参数,并且只有对最终结果感兴趣的调用者才需要与回调函数厮混在一起。

其次,Promise API 标准化了回调函数的传递。所有库可能会把 callbacks 和 errbacks 参数放到函数签名的不同位置。某些库甚至不接受一个 errback 参数。大部分 不接受一个 alwaysback(即“finally”)。我们可以依赖 Promise API,而不是许多有着潜在差异的库 API

第三,Promise 保障了回调函数和错误回调函数被调用的方式和时机,以及如何处理返回值和回调函数抛出的异常。在没有 Promise 的世界里,如果库和函数签名支持许多不同的回调函数,便意味着许多不同的行为:

  1. 你的回调函数允许返回一个值吗?
  2. 如果允许返回会发生什么?
  3. 是否所有库都允许你的回调函数抛出一个异常?如果允许抛出会发生什么?悄悄的把它吞掉吗?
  4. 如果你的回调函数真的抛出一个异常,错误回调是否会被调用?

...等等...

所以,当考虑把 Promises 作为回调函数注册的标准 API 时,也为如何以及何时调用回调函数和处理异常提供了标准的、可预测的行为。

怎么处理 try/catch/finally?

现在,我们已经恢复了调用-返回编程模型,并从函数签名中移除了回调函数,我们还需要一种方式来处理失败的情况。理想情况下,我们希望使用 try/catch/finally,或者是至少在外观和行为上与它相似,并且在面对异步时可以正常工作。

用 Promises 控制异步错误处理 一文中,我们将把拼图的最后一块填到位,看看如何用 Promises 模仿 try/catch/finally。


原文:Simplifying Async with Promises



blog comments powered by Disqus