Welcome

首页 / 软件开发 / 数据结构与算法 / Promise/A的误区以及实践

Promise/A的误区以及实践2014-06-11 infoq 李光毅什么是Promise

Promise是一种让异步代码书写起来更优雅的模式,能够让异步操作代码像同 步代码那样书写并且阅读,比如下面这个异步请求的例子:

$.get("/js/script,js", function () {// callback to do})
就可以改写为Promise模式:

var promise = $.get("/js/script");
返回值promise即代表操作的最终结果。返回值promise也可以作为“第 一类对象”(First-class object)被当做参数传递。这个模式最大的优势 就是避免了传统异步操作代码中,回调函数嵌套回调函数的糟糕情况。

如果你之前对Promise模式有所了解的话(可以参考InfoQ之前的这篇文章), 谈到Promise,最先想到的一定是它的then函数,的确它非常重要,在Promise模 式的定义中(CommonJS Promises/A)中,then函数是这么被定义的:

(原文)A promise is defined as an object that has a function as the value for the property then: then(fulfilledHandler, errorHandler, progressHandler)

(译)一个promise被定义为一个拥有then属性的对象,并且此then属性的值 为一个函数: then(fulfilledHandler, errorHandler, progressHandler)

也就是说每一个promise结果一定会自带一个then函数,通过这个then函数, 我们可以添加promise转变到不同状态(定义中promise只有三种状态, unfulfilled, fulfilled, failed.这里说的状态转变即从unfulfilled至 fulfilled,或者从unfulfilled至failed)时的回调,还可以监听progress事件, 拿上面的代码为例:

var fulfilledHandler = function () {}var errorHandler = function () {}var progressHandler = function () {}$.get("/js/script").then(fulfilledHandler, errorHandler, progressHandler)
这有一些类似于

$.ajax({error: errorHandler,success: fulfilledHandler,progress: progressHandler})
这个时候你会感到疑惑了,上面两种方式看上去不是几乎一模一样吗? ——但promise的重点并非在上述各种回调函数的聚合,而是 在于提供了一种同步函数与异步函数联系和通信的方式。之所以感到相 似这也是大部分人对Promise的理解存在的误区,只停留在then的聚合 (aggregating)功能。甚至在一些著名的类库中也犯了同样的错误(下面即以 jQuery举例)。下面通过列举两个常见的误区,来让人们对Promise有一个完整的 认识。

Promise/A模式与同步模式有什么联系?

抛开Promise,让我们看看同步操作函数最重要的两个特征

能够返回值

能够抛出异常

这其实和高等数学中的复合函数(function composition)很像:你可以将一个 函数的返回值作为参数传递给另一个函数,并且将另一个函数的返回值作为参数 再传递给下一个函数……像一条“链”一样无限的这么 做下去。更重要的是,如果当中的某一环节出现了异常,这个异常能够被抛出, 传递出去直到被catch捕获。

而在传统的异步操作中不再会有返回值,也不再会抛出异常——或 者你可以抛出,但是没有人可以及时捕获。这样的结果导致必须在异步操作的回 调中再嵌套一系列的回调,以防止意外情况的发生。

而Promise模式恰好就是为这两个缺憾准备的,它能够实现函数的复合与异常 的抛出(冒泡直到被捕获)。符合Promise模式的函数必须返回一个promise,无 论它是fulfilled状态也好,还是failed(rejected)状态也好,我们都可以把它当 做同步操作函数中的一个返回值:

$.get("/user/784533") // promise return.then(function parseHandler(info) {var userInfo = parseData(JSON.parse(info));return resolve(userInfo); // promise return}).then(getCreditInfo) // promise return.then(function successHandler(result) {console.log("User credit info: ", result);}, function errorHandler(error) {console.error("Error:", error);})
``` 上面的例子中,$.get与getCreditInfo都为异步操作,但在Promise模式 下,(形式上)转化为了链式的顺序操作

$.get返回的promise由parseHandler进行解析,返回值“传入 ”getCreditInfo中,而getCreditInfo的返回值同时“传入 ”successHandler中。

之所以要在传入二字上注上引号,因为并非真正把promise当做值传递进入函 数中,但我们完全可以把它理解为传入,并且改写为同步函数的形式,这样以来 函数复合便一目了然:

try {var info = $.get("/user/784533"); //Blockingvar userInfo = parseData(JSON.parse(info));var resolveResult = parseData(userInfo);var creditInfo = getCreditInfo(resolveResult); //Blockingconsole.log("User credit info: ", result);} cacth(e) {console.error("Error:", error);}
但是在jQuery1.8.0版本之前,比如jQuery1.5.0(jQuery在1.5.0版本中引入 Promise,在1.8.0开始得到修正),存在无法捕获异常的问题:

var step1 = function() {console.log("------step1------");var d = $.Deferred();d.resolve("Some data");return d.promise();},step2 = function(str) {console.log("------step2------");console.log("step2 recevied: ", str);var d = $.Deferred();// 故意在fulfilled hanlder中抛出异常d.reject(new Error("This is failing!!!"));return d.promise();},step3 = function(str) {console.log("------step3------");console.log("step3 recevied: ", str);var d = $.Deferred();d.resolve(str + " to display");return d.promise();},completeIt = function(str) {console.log("------complete------");console.log("[complete]------>", str);},handleErr = function(err) {console.log("------error------");console.log("[error]------>", err);}; step1().then(step2).then(step3).then(completeIt, handleErr);
上述代码在jQuery-1.5.0中运行的结果:

------step1------------step2------step2 recevied:Some data------step3------step3 recevied:Some data------complete------[complete]------> Some data