用 Promises 简化异步编程
原文链接 https://bubkoo.github.io/2014/03/25/simplifying-async-with-promises/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
在凌乱的异步编程一文中,我们见识了用回调处理异步调用的尴尬局面,即使是一组简单的函数调用。
快速回顾一下,看看我们最初的代码,使用回调函数时的凌乱结果,以及我们为了回到正途而想要解决的几个问题:
- 我们再也不能使用简单的“调用 - 返回”(call-and-return)编程模型
- 我们再也不能使用 try/catch/finally 来处理异常
- 我们必须为可能执行异步操作的每个函数的签名增加 callback 和 errback 参数
1. Promises
一个 Promise(或者叫 Future, Delayed value, Deferred value)代表一个尚不可用的值,因为产生这个值的计算过程尚未完成。一个 Promise 是最终的成功结果或失败原因的占位符。
Promises 还提供了一个简单的 API(见下文),用于在结果完成或故障发生时获取通知。
Promises 不是一个新概念,已经在许多语言中被实现。一些 JavaScript 实现也已经有一段时间了,并且最近变得更加流行,因为我们开始构建更庞大、更复杂的系统,需要协调更多的异步任务。
(注意:虽然 Promise API 标准存在多个提案,但是 Promises/A+ 已经在多个主流框架中被实现,似乎正在成为事实上的标准。无论哪种提案,基本的概念是相同的:1) Promises 作为结果或错误的占位符;2) 提供了一种在结果完成或错误发生时的通知方式。)
<!--more-->
2. 典型的 XHR 示例
在 XHR Get 示例中,我们关注的是请求 url 的内容。我们知道 XHR 是一个异步操作,返回值不会立即可用。这种情况完全符合 Promise 的定义。
假设我们有一个 XHR 库,它立即返回一个 Promise 作为内容的占位符,而不是要求我们传入一个回调函数。我们可以重写 Part 1 中的异步函数 thisMightFail
,像下面这样:
function thisMightFail() {
// XHR 库将返回一个 Promise 作为内容的占位符
// XHR 本身将在稍后执行
var promise = xhrGet('/result');
// 我们可以简单地返回这个 Promise,它就像真正的结果
return promise;
}
(需要注意的是,一些流行的 JavaScript 库,包括Dojo(参考 @bryanforbes 写的 great article on Dojo's Deferred)和 jQuery,都使用了 Promises 来实现 XHR 操作)
现在,我们可以返回 Promise 占位符,就像它是真正的结果,这样异步函数 thisMightFail
看起来非常像传统的同步操作和“调用 - 返回”编程模式。
3. 调用栈
在没有回调函数的世界里,结果和错误沿着调用栈向上回传。这是一种符合预期和友好的模式。而在基于回调函数的世界里,正如我们之前看到的那样,结果和错误不再遵循这种熟悉的模式,回调函数必须向下传递,深入到调用栈中。
通过使用 Promises,我们可以恢复到熟悉的“调用 - 返回”编程模型,并移除回调函数。
3.1 回到“调用 - 返回”编程模型
为了看看它是如何工作的,让我们从 Part 1 中同步函数 getTheResult
的简化版本开始,不使用 try/catch,这样异常将总是沿着调用栈向上传播。
function thisMightFail() {
//...
if(badThingsHappened) {
throw new Error(...);
}
return theGoodResult;
}
function getTheResult() {
// 返回 thisMightFail 的执行结果
// 或者让异常抛出
return thisMightFail();
}
现在,让我们使用基于 Promise 的 XHR 库,来为上面的代码引入异步的 thisMightFail
:
function thisMightFail() {
// XHR 库将返回一个 Promise 作为内容的占位符
// XHR 本身将在稍后执行
var promise = xhrGet('/result');
// 我们可以简单地返回这个 Promise,它就像真正的结果
return promise;
}
function getTheResult() {
// 返回 thisMightFail 的执行结果,它是一个 Promise
// 代表将来的执行结果或错误
return thisMightFail();
}
使用 Promises 时,getTheResult()
在同步和异步情况下是相同的!并且在这两种情况下,成功结果或失败将沿着调用栈传播到调用者。
3.2 移除回调函数
还请注意,没有向调用栈传入 callbacks 或 errbacks(或 alwaysbacks),也没有污染任何函数的签名。通过使用 Promises,我们的函数的外观和行为就像友好的、同步的“调用 - 返回”编程模型。
3.3 完成了吗?
我们已经使用 Promises 重构了简单的 getTheResult
函数,并且解决了在 Part 1 提出的的两个问题。我们已经:
- 回到了“调用 - 返回”编程模型
- 移除了参数 callback/errback/alwaysback 的传播
但是,对于 getTheResult
的调用者意味着什么呢?别忘了,我们返回的是一个 Promise,并且无论成功结果(XHR 的结果)还是错误最终将落实到占位符 Promise,到那时调用者将需要采取一些行动。
4. 调用者该如何处理
正如上面所提到的,Promises 提供了一个 API,用于在结果可用或错误时获取通知。例如,在 Promises/A 规范提案中,一个 Promise 含有一个 .then()
方法,而且许多 Promise 库提供了一个 when()
方法来达到同样的目的。
首先,让我们看看使用回调方式时,调用代码可能的样子:
// 基于回调的 getTheResult
getTheResult(
function(theResult) {
// theResult 将是 XHR 的响应结果
resultNode.innerHTML = theResult;
},
function(error) {
// error 表示 XHR 失败的原因
// 例如,它可能是一个 Error 对象
errorNode.innerHTML = error.message;
}
);
现在,让我们如何通过 Promises/A 的 .then()
来使用使用 getTheResult
所返回的 Promise。
getTheResult().then(
function(theResult) {
// theResult 将是 XHR 的响应结果
resultNode.innerHTML = theResult;
},
function(error) {
// error 表示 XHR 失败的原因
// 例如,它可能是一个 Error 对象
errorNode.innerHTML = error.message;
}
);
这就是 Promises 用来避免使用回调函数的全部内容?我们就这么使用它们?!?
5. 还没结束
在 JavaScript 中,通过使用回调函数来实现 Promises,因为没有语言级的结构可以用于处理异步。回调函数是 Promises 必然的实现方式。如果 Javascript 已经提供或者未来可能提供其他的语言结构,那么 Promises 可能会以不同的方式实现。
然而,相较于 Part 1 中深度传递回调函数的方式,Promises 具备一些明显的优势。
首先,我们的函数签名是正常的。我们不再需要为从调用者到 XHR 库的每个函数签名添加 callback 和 errback 参数,只需要为对最终结果感兴趣的调用者传递回调函数。
其次,Promise API 标准化了回调函数的传递。JavaScript 库可能会把 callbacks 和 errbacks 参数放到函数签名的不同位置,某些库甚至不接受 errback 参数,而且大部分库不接受 alwaysback(即“finally”)参数。我们可以依赖 Promise API,而不是许多有着潜在差异的库的 API。
第三,Promise 保障了回调函数和错误回调函数被调用的方式和时机,以及如何处理返回值和回调函数抛出的异常。在没有 Promise 的世界里,如果库和函数签名支持许多不同的回调函数,便意味着许多不同的行为:
- 你的回调函数允许返回一个值吗?
- 如果允许返回会发生什么?
- 是否所有库都允许你的回调函数抛出一个异常?如果允许抛出会发生什么?悄悄的把它吞掉吗?
- 如果你的回调函数真的抛出一个异常,错误回调是否会被调用?
...等等...
所以,Promises 一方面可以作为回调函数注册的标准 API,同时也为如何以及何时调用回调函数和处理异常提供了标准的、可预测的行为。
6. 怎么处理 try/catch/finally
现在,我们已经回到了“调用 - 返回”编程模型,并从函数签名中移除了回调函数,我们还需要一种方式来处理失败的情况。理想情况下,我们希望使用 try/catch/finally,或者是至少在外观和行为上与它相似,并且在面对异步时可以正常工作。
在用 Promises 控制异步错误处理一文中,我们将把拼图的最后一块填到位,看看如何用 Promises 模仿 try/catch/finally。