什么是 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"); //Blocking var userInfo = parseData(JSON.parse(info)); var resolveResult = parseData(userInfo); var creditInfo = getCreditInfo(resolveResult); //Blocking console.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
在 step2 中,在解析 step1 中传递的值后故意抛出了一个异常,但是我们在最后定义的errorHandler
却没有捕获到这个错误。
忽略捕获异常的错误,上面的结果还反映出另一个问题,最后一步completeHandler
中处理的值应该是由 step3 中决定的,也就是 step3 中的
d.resolve(str + ' to display');
最后应打印出的结果为
some data to display
而在 jQuery-1.9.0 中异常是可以捕获的,运行结果为:
------step1------ ------step2------ step2 recevied: Some data ------error------ [error]------> Error {}
但是打印出的结果仍然有问题
注意到 step3 没有执行,因为 step3 中只定义了 fulfilled 的回调,异常只有在最后errorhandler
才被捕获。
其实我们可以试试,在 step3 中添加处理异常的回调函数:
step1(). then(step2). then(step3, function (str) { console.log("------[error] step3------"); console.log("step3 revecied: ", str); var d = $.Deferred(); d.resolve(str + ' to display'); return d.promise(); }). then(completeIt, handleErr);
运行结果如下:
------step1------ ------step2------ step2 recevied: Some data ------[error] step3------ step3 revecied: Error {} ------complete------ [complete]------> Error: This is failing!!! to display
虽然错误在 step3 被捕获了,但是由于我们将错误信息传递了下去,最后一步打印出的仍然是 error 消息
细节:返回 Promise
让我们继续看看 Promise/A 定义的第二段:
(原文)This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.
(译文)这样的函数应该返回一个新的 promise,该 promise 是被指定回调函数 (成功执行或者捕获异常) 解析之后的结果。如此一来 promise 之间的操作便能链式的串联起来。回调函数返回的值是解析返回的 promise 的结果。如果回调函数抛出了异常,返回的 promise 便会转化为异常状态
这段定义告诉我们两点:
- 无论返回值是 fulfilled 也好,还是被 rejected 也好,必须返回一个新的 promise;
then
关键字并非只是各个回调的填充器,在输入的同时它同时也输出新的 promise,以便形成链式 ;
同样以 jQuery 的代码为例:
var step1 = function() { console.log("------step1------"); var d = $.Deferred(); d.resolve('Some data'); return d.promise(); }; var step2 = function (result) { console.log("------step2------"); console.log("step2 recevied: " + result); var d = $.Deferred(); d.resolve("step2 resolve: " + result); return d.promise(); } var step3 = function (result) { console.log("------step3------"); console.log("step3 recevied: " + result); var d = $.Deferred(); d.reject(new Error("This is failing!!!")); return d.promise(); } var promise = step1(); var promise1 = promise.then(step2); var promise2 = promise.then(step3);
var promise1 = promise.then(step2); var promise2 = promise.then(step3); ``` step1 返回的 promise 是 fulfilled 状态,但不同的是 step2 fulfilled 之后,返回一个仍然可被解析的 promise(1),而 step3 则抛出一个异常 (promise2)。
按照定义所说,promise1 与 promise2 是相互不同的 promise,无论是被正确解析还是抛出异常,返回的都应该是一个独立的 promise。
为了验证产生的是否为独立的 promise,只需看他们的执行结果如何,接着给 promise1 和 promise2 定义 fulfilled 和 failed 回调函数:
promise1.then(function (result) { console.log("Success promise1: ", result); }, function () { console.log("Failed promise1: ", result); }) promise2.then(function (result) { console.log("Success promise2: ", result); }, function (result) { console.log("Failed promise2: ", result); })
让我们看看在 jQuery-1.5.0 中执行的结果:
Success promise1: Some data Success promise2: Some data
虽然是一个被抛出的异常,但仍然可以被正确解析,并且解析使用的参数是上一个 promise 的返回值
在 jQuery-1.9.0 中:
Success promise1: step2 resolve: Some Failed promise2: Error {}
能被正常解析。
实践
完整认识了 promise 之后,我们可以用简单的代码实现一个 Promise 模式。
参照 jQuery 的Deferred
,我们可以了解 Promise 的大致结构:
var Promise = function () {} Promise.prototype.when = function () { // to do } Promise.prototype.resolve = function () { // to do } Promise.prototype.rejected = function () { // to do }
Promise.prototype.rejected = function () { // to do } ``` 并且我们用最简单一个异步操作setTimeout
来验证我们的 Promise 是否奏效:
var delay = function (throwError) { var promise = new Promise(); if (throwError) { promise.reject(new Error("ERROR")); return promise; } setTimeout(function () { promise.resolve("some data"); }, 1000); return promise; } delay().then(function (result) { console.log(result); }).then(function () { console.log("This is the second successHandler"); }).then(function () { console.log("This is the third successHandler"); })
首先我们要为每一个 Promise 准备一个队列来存储自己的回调函数
function Promise() { this.callbacks = []; }
我们可以暂且把then()
理解为往队列中填入回调的函数,并且为了能以链式的形式添加处理函数,最后必须返回当前 promise:
Promise.prototype.then = function (successHandler, failedHandler) { this.callbacks.push({ resolve: successHandler, reject: failedHandler }); return this; }
} ``` 其实 resolve 和 reject 虽然名称不同,但是都是执行各自对应的回调函数,于是可以抽象出一个公共的complete
方法:
Promise.prototype = { resolve: function (result) { this.complete("resolve", result); }, reject: function (result) { this.complete("reject", result); }, complete: function (type, result) { // to do } }
complete 的工作非常显而易见,根据 type 不同执行回调函数出队,以 result 为参数,执行相应 type 的的函数:
complete: function (type, result) { this.callbacks.shift()[type](result); }
但是这样只能执行队首回调函数,在链式的情况下,可能在callbacks
中添加了多个回调函数,为了实现链式的执行,需要把callbacks
中的回调全部出队,complete
可以改进为:
complete: function (type, result) { while(this.callbacks.length) { this.callbacks.shift()[type](result); } }
完整版如下:
function Promise() { this.callbacks = []; } Promise.prototype = { resolve: function (result) { this.complete("resolve", result); }, reject: function (result) { this.complete("reject", result); }, complete: function (type, result) { while(this.callbacks[0]) { this.callbacks.shift()[type](result); } }, then: function (successHandler, failedHandler) { this.callbacks.push({ resolve: successHandler, reject: failedHandler }); return this; } }
第一个版本即完成,可以看到测试上面开始例子的结果,能够顺利打印出信息。
接下来我们来完成处理异常部分
首先我们写一个能够故意抛出异常的测试用例
delay(true).then(function (result) { console.log(result); }, function firstErrorHandler(error) { console.error("First failedHandler catch: ", error); var promise = new Promise(); promise.resolve("some data"); return promise; }).then(function secondSucHandler(result) { console.log("Second successHandler recevied: ", result); }, function (error) { console.error("Second failedHandler catch: ", error); })
我们在 delay 中抛出异常,希望在firstErrorHandler
捕获异常后,返回一个能 fulfilled 的 promise,并且用secondSucHandler
顺利解析、
如果直接用上面版本执行,会发现没有任何结果,为什么?
** 因为在执行delay()
时,第一个reject
也同时被执行,但此时then
函数还没执行,也就是处理 reject 的 handler 还没有被定义。** 当然也就不会有任何结果了。反过来也能想通,也就能说通 resolve 能被执行。
那么我们只要在 reject 函数中加上一定的延时即可:
... reject: function (result) { var _this = this; setTimeout(function () { _this.complete("reject", result); }); }, ...
执行测试代码结果如下:
First failedHandler catch: Error Second failedHandler catch: Error
虽然错误被捕获了,但错误被一直传递一下去了,这也就是我们之前说的 jQuery 无法返回新的 promise,接下来要解决这个问题。
我们来写一个更复杂的测试用例,来验证下面的解决方案:
delay() // ------Level 1------ .then(function FirstSucHandler(result) { console.log("First successHandler recevied: ", result); var p = new Promise(); p.reject(new Error("This is a test")); return p; }, function FirstErrorHandler(error) { console.error("Second failedHandler catch: ", error); }) // ------Level 2------ .then(function SecondSucHandler(result) { console.log("Second successHandler recevied: ", result); }) // ------Level 3------ .then(function ThirdSucHandler(result) { console.log("Third successHandler recevied: ", result); }, function ThirdErrorHandler(error) { console.error("Third failedHandler catch: ", error); }) // ------Level 4------ .then(function FourSucHandler(result) { console.log("Fourth successHandler recevied: ", result); })
正确的执行顺序应该是FirstSucHandler
fulfilled 之后抛出异常,略过SecondSucHandler
,异常被ThirdErrorHandler
捕获,并且返回一个新的 promise,由FourSucHandler
解析。
接下来还要修复并且考虑这些问题 1. 当异常被捕获之后将阻止异常往下传递 2. 定义中描述在 fulfilled 之后必须返回一个新的 promise,但如果没有返回新的 promise,或者是返回其他的值,应该作何处理?
对于第二点,我们暂且处理规则是:
- 如果没有返回值,那么下一个回调函数将继续解析上一 promise
- 如果返回值存在,但返回值不为 promise,默认调用 resolve handler,并且返回值作为回调函数的参数传入
大致流程图如下所示
push |{1} [successHandler, errorHandler] ... ------->[successHandler, errorHandler] | | | shift | | | execute | succussHandler or errorHandler | | | | | ---------------------- | | | | return return ----Auto--promise nothing or not promise | | <--------------- Manual --------|
``` 从上图中可以看出,promise 模式是一个周而复始执行 resolve 或者 reject 过程,每一轮必须执行二者之一,必然导致一组回调函数出队。如果抛出异常,但这组回调函数中没有异常处理函数errorHandler
,那么这组回调函数便作废,直到找到下一个能捕获异常的回调函数。直到队列中回调函数全部出队。看来我们有必要写一个“直到找到我们需要的函数”的函数:
getCallbackByType: function (type) { if (callbacks.length) { var callback = callbacks.shift()[type]; while (!callback) { callback = callbacks.shift()[type]; } } return callback; }
从上图中可以看出所有的 promise 可以共用一个callbacks
队列,并且考虑到需要判断返回值是否为 promise 类型,我们最好还需要一个标志位,做如下修改:
var callbacks = []; function Promise() { this.isPromise = true; } ...
根据以上描述,这类似于一个递归的过程
promise --> resolve/reject ---> promise ---> resolve/reject
注意在上面 Level 1 的FirstSucHandler
中,新返回的 promise 执行了 reject,这会自动使队列一组回调函数出队并执行。但面对一些没有返回值的情况应该怎么办,那么就应该遵循我上面说的标准,要么执行上一个 promise,要么默认执行下一个 resolve:
... executeInLoop: function (promise,result) { // 1. 如果回调队列还没有被清空 // 2. 或者没有返回值, // 3. 或者有返回值但不是 promise if ((promise && !promise.isPromise || !promise) && callbacks.length) { // 默认执行 resolve var callback = this.getCallbackByType("resolve"); if (callback) { var promise = callback(promise? promise: result); this.executeInLoop(promise, promise? promise: result); } } }, ...
最后 Complete 函数也要做相应修改:
... complete: function (type, result) { var callback = this.getCallbackByType(type); if (callback) { var promise = callback(result); this.executeInLoop(promise, promise? promise: result); } }, ...
最后贴上完整版代码:
var callbacks = []; function Promise() { this.isPromise = true; } Promise.prototype = { resolve: function (result) { this.complete("resolve", result); }, reject: function (result) { var _this = this; setTimeout(function () { _this.complete("reject", result); }); }, executeInLoop: function (promise,result) { // 如果队列里还有函数 并且( 要么 没有返回一个值 或者 (有返回值但不是 promise 类型)) if ((promise && !promise.isPromise || !promise) && callbacks.length) { var callback = this.getCallbackByType("resolve"); if (callback) { var promise = callback(promise? promise: result); this.executeInLoop(promise, promise? promise: result); } } }, getCallbackByType: function (type) { if (callbacks.length) { var callback = callbacks.shift()[type]; while (!callback) { callback = callbacks.shift()[type]; } } return callback; }, complete: function (type, result) { var callback = this.getCallbackByType(type); if (callback) { var promise = callback(result); /* 1. 有返回值,promise 类型 2. 有返回值,其他类型 3. 无返回值 */ this.executeInLoop(promise, promise? promise: result); } }, then: function (successHandler, failedHandler) { callbacks.push({ resolve: successHandler, reject: failedHandler }); return this; } }
并附上执行结果:
First successHandler recevied: some data Third failedHandler catch: Error {} Fourth successHandler recevied: Error {}
参考文献
- Promises/A+ - understanding the spec through implementation
- Promise patterns
- You’re Missing the Point of Promises
- Creating Responsive Applications Using jQuery Deferred and Promises
- Asynchronous Programming in JavaScript with “Promises”
- Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.
- tiny Promise.js
作者简介
李光毅,新晋前端工程师,现就职于爱奇艺,热于前端技术分享。联系邮箱:juststayinvegas@gmail.com
评论