原文地址: http://www.2ality.com/2014/10/es6-promises-api.html
原文作者:Dr. Axel Rauschmayer
译者:倪颖峰
原博客已经标明:本博客文档已经过时,可进一步阅读“Exploring ES6”中的 “Promises for asynchronous programming”。仔细对比了下,两者的确存在一些差异。本文是在原来的译文基础上修订的。
博文下半部分为纯干活内容,包括介绍的 ES6 中 Promise API,以及其简单的实现方式与思想,以及一些拓展内容。
9. 速查表:ES6的 Promise API
这里简单的给出一下 ES6 Promise 的API 简述,详细描述请看规范http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects
9.1. 术语
Promise 的 API 就是关于异步获取结果。一个Promise 对象(简称Promise)是一个独立的对象,会通过该对象传递结果。
状态:
一个Promise处于以下三个(互斥)状态中的某一个状态:
1. 结果未完成时,Promise 状态为 pending。
2. 得到结果时,Promise 状态为 fulfilled。
3. 当错误发生时,Promise 状态为 rejected。
一个Promise 一旦被设置(settled)则表示“事情完成”(包括 fulfilled 或者 rejected)。
一个Promise只能设置一次,之后便不能再改变。
状态变化反应:
Promise reactions 就是用 Promise 方法 then( ) 来注册的回调函数,用来通知fulfillment 或 rejection。
一个 then对象(thenable)即为一个含有Promise 风格的then( )方法的对象。当一个API只对settlement的通知感兴趣,那么它只需要then对象。
状态改变:
有两个操作会改变Promise 状态。一旦你调用了其中的一个,那么进一步调用将会无效。
。Rejecting一个Promise 表示这个promise状态变为rejected。
。Resolving一个Promise 有两种情况,取决于以什么值来解析:
1. 以一个普通值(非 then对象)来解析,则fulfillment给 Promise。
2. 以一个 then对象T来解析Promise P ,意味着P 不再可以被解析,而要随着T的状态走,即T fulfillment或rejection的值。一旦T被设置时P的reactions就被触发(或者如果T已经被设置则立即触发)。
9.2. Promise构造函数
Promise 的构造函数这样调用:
let p = new Promise(function(resolve, reject) {...} );
构造函数的回调函数被称为executor,executor使用参数来resolve或者reject new出来的Promise p:
。resolve(x),以 x 来解析 p:
如果 x 是 then对象,那么它的settlement会转给 p(包括通过 then() 注册的reactions)。
否者,p 就被赋值为 x。
。reject(x),以 e 值来拒绝 p(e通常为一个 Error实例)。
9.3. 静态方法
所有的Promise 的静态方法都支持子类化(subclassing):
。通过species pattern来创建新的Promise实例
. 缺省使用receiver (this)作为构造函数。
. 缺省方式可以被子类中property [Symbol.species]覆盖。
。还可以通过species pattern(不是通过Promise或this)访问其他静态方法。
9.3.1 创建 Promises
下面两个静态方法创建接收器的新实例。
1. Promise.resolve(x): 将任意值转换为Promise对象。
。如果 x 是 then对象,它将转换为一个Promise(接收器的一个实例),这里没有用species pattern。
。如果 x 已经是接收器的一个实例,则不改变返回。
。否则,将返回接收器的一个新实例,并且以 x 来填充。
2. Promise.reject(reason):创建接收器的一个新实例(通过species pattern来配置),并且以 reason 值来处理拒绝状态。
9.3.2 组合 promises
直观来说,静态方法 Promise.all() 和 Promise.race() 将可遍历的Promises 组合为一个Promise。即:
1. 它们接受一个可遍历的对象iterable。iterable的元素通过 this.resolve()被转换为Promises。
2. 它们返回一个新的Promise。这个Promise 是接收器的一个新实例(通过species pattern来配置)。
下面是这两个方法:
1. Promise.all(iterable):返回一个Promise
。如果iterable每一个元素的状态均为 fulfilled,那么将其状态设置为 fulfilled。
fullfillment值:由每一个填充值组成的数组。
。当元素中有任何一个状态为rejected时,那么将其状态设置为 rejected。
rejection值:第一个rejection值。
2. Promise.race(iterable):iterable中第一个被设置的元素就是返回的Promise。
9.4. Promise.prototype方法
9.4.1 Promise.prototype.then(onFulfilled, onRejected)
。回调函数 onFulfilled 和 onRejected 被称为 reactions。
。如果Promise状态已经被设为 fulfilled,或者一旦变成 fulfilled,那么回调函数 onFulfilled 就会被立即执行。类似地,onRejected是在状态变为rejected时触发。
。then()方法返回一个新的Promise Q(通过接收器构造函数的species创建):
. 如果有一个reaction返回结果,Q 便使用其来resolve。
. 如果有一个reaction抛出异常,Q 便使用其来reject。
。被忽略的reactions:
. 如果 onFulfilled被忽略,那么接收器的fulfillment值就会被转发到 then() 的结果中。
. 如果 onRejected被忽略,那么接收器的rejection值就会被转发到 then() 的结果中。
被忽略的reactions的默认值可以以下方式来实现:
function defaultOnFulfilled(x){
return x;
}
function defaultOnRejected(e){
throw e;
}
9.4.2 Promise.prototype.catch(onRejected)
p.catch(onRejected)等同于p.then(null, onRejected)。
10. promises 的优势与劣势
10.1. 优势
统一的异步 APIs
Promises 的一个重要的优点就是被越来越多的浏览器异步 API 使用,统一了当前多样化和不兼容的各种模式和约定。看一下即将出现的两种基于Promise 的API。
基于 Promise 的 fetch API 替代 XMLHttpRequest:
fetch(url)
.then(request => request.text())
.then(str => ...)
fetch() 会对实际的请求返回一个 Promise,text() 会返回一个内容为字符串的Promise。
ECMAScript 6 API 编程式导入模块也是基于 Promise:
System.import('some_modle.js')
.then(some_module => {
...
})
Promises VS events
对比 events,Promises 更适合做一次性结果的监听。不管是在结果计算之前还是之后注册,都不会影响监听到结果。这是 Promises根本上的优势。另一方面,你不能用它来处理一些反复性的事件。链式是 Promise 的另一个优势,但是每次只能添加一个。
Promises VS callbacks
对比回调函数,Promises 具有更简洁的函数(方法)参数。在回调函数中,参数包含输入参数和输出参数:
fs.readFile(name, opts?, (err, string | Buffer) => void)
而 Promise 中所有的参数均只是输入参数:
readFilePromisified(name, opts?) : Promise<string | Buffer>
此外,Promises 的优势还包括更好的错误处理机制(集成了异常处理,组合更容易(因为你可以重用一些同步工具,比如 Array.prototype.map())。
10.2. 劣势
Promises 对于单一的异步结果能处理的很好。但是它不擅长于:
1. 反复性事件:如果你对此感兴趣,可以看一下 reactive programming(http://reactive-extensions.github.io/RxJS/),关于链式处理普通事件处理的巧妙方式。
2. 数据流:支持数据流的标准尚在开发中。
ECMAScript 6 的 Promises 缺少以下两个某些时候很有用的功能点:
1. 你无法取消。
2. 你无法确定他们发生多长时间了(例如在客户端用户界面展现进度条)。
Q Promises 库支持后者,而且Promises/A+也将计划添加这两个功能点 。
11. Promises与生成器
下面使用控制流库co(https://github.com/tj/co)异步获取两个JSON文件。注意在行A,一个执行块在等待直到得到Promise.all()的结果。这使得代码看起来类似于同步而实际上在执行异步操作。
co(function* () {
try {
let [croftStr, bondStr] = yield Promise.all([ // (A)
getFile('http://localhost:8000/croft.json'),
getFile('http://localhost:8000/bond.json'),
]);
let croftJson = JSON.parse(croftStr);
let bondJson = JSON.parse(bondStr);
console.log(croftJson);
console.log(bondJson);
} catch (e) {
console.log('Failure to read: ' + e);
}
});
这段代码的细节在生成器章节中解释。
12. 调试 promises
浏览器中开始慢慢出现可调试Promise的工具。可快速看下最新Google Chrome中的表现,下面是一段代码演示两个常见的Promise问题:
<body>
<script>
// Unhandled rejection
Promise.reject(new Error())
.then(function (x) { return 'a'})
.then(function (x) { return 'b'})
// Unsettled Promise
new Promise(function () {});
</script>
</body>
第一个问题是没有处理rejection,第二个问题是Promise未被设置。Chrome开发工具可协助检查这两个问题:
在console中可以看到未处理的rejection:
同时在源码检查器中也会高亮显示:
而且,还有针对Promises的特殊检查器,记录web页面创建的所有Promises。它会记录:
哪些Promises状态是fulfilled (第一列的绿球), rejected (红球)和pending (灰球)
Promises是如何链在一起的
13. promises 内部窥秘
本章节,我们将从不同的角度来看一下Promises:不再学习如何使用 promise API,我们将了解一下它的简单实现。这个视角将帮助我们更好地理解Promises。
下面这个Promise 的实现被称为 DemoPromise 和 在 GitHub 上可用的实例(https://github.com/rauschma/demo_promise)。为了更容易理解,将不会完全匹配 API。但是它将足够使你了解在实现Promises 过程会中面对的各种难题。
DemoPromises 是一个含有三个原型方法的类:
1. DemoPromises.prototype.resolve(value)
2. DemoPromises.prototype.reject(reason)
3. DemoPromises.prototype.then(onFulfilled, onRejected)
在此,resolve 和 reject 为方法(而不是传递给构造函数作为参数的回调函数)。
13.1 一个独立的Promise
首先我们需要实现一个最少功能的独立的Promise :
1. 你可以创建一个Promise。
2. 你可以resolve或者reject一个Promise 并且只能处理它一次。
3. 你可以通过 then() 来注册 reactions(回调函数)。该方法不支持链式(该处应该是支持链式),就是说,其不返回任何东西。不管Promise 是否已经被处理该方法都可以被调用。
以下是第一个实现:
let dp = new DemoPromise();
dp.resolve('abc');
dp.then(function(value){
console.log(value); // abc
});
下面图说明了我们第一个 DemoPromise 是如何实现的:
13.1.1 DemoPromise.prototype.then()
让我们首先研究下 then()。它必须处理两个情况:
1. 如果Promise 状态还是pending,那么就排队onFulfilled 与onRejected的调用,直到Promise被处理完毕(settled)之后调用。
2. 如果Promise状态变为fulfilled或者rejected,则onFulfilled 或者 onRejected 将被立即调用。
then(onFulfilled, onRejected) {
let self = this;
let fulfilledTask = function () {
onFulfilled(self.promiseResult);
};
let rejectedTask = function () {
onRejected(self.promiseResult);
};
switch (this.promiseState) {
case 'pending':
this.fulfillReactions.push(fulfilledTask);
this.rejectReactions.push(rejectedTask);
break;
case 'fulfilled':
addToTaskQueue(fulfilledTask);
break;
case 'rejected':
addToTaskQueue(rejectedTask);
break;
}
}
function addToTaskQueue(task) {
setTimeout(task, 0);
}
13.1.2 DemoPromise.prototype.resolve()
resolve() 工作原理如下:如果Promise 已经被处理完毕(settled),将不做任何事情(确保一个Promise 只被处理一次)。否则,Promise 的状态转变为 fulfilled 并且结果缓存到 this.promiseResult。队列中所有的fulfillment reactions 将被立即执行。
resolve(value) {
if (this.promiseState !== 'pending') return;
this.promiseState = 'fulfilled';
this.promiseResult = value;
this._clearAndEnqueueReactions(this.fulfillReactions);
return this; // enable chaining
}
_clearAndEnqueueReactions(reactions) {
this.fulfillReactions = undefined;
this.rejectReactions = undefined;
reactions.map(addToTaskQueue);
}
reject() 与 resolve() 类似。
13.2 链式(注意,这部分已经为下一步扁平化做好了基础)
接下来,我们将实现链式:
1. then() 返回的 Promise 实际上是 onFulfilled 或者 onRejected 被执行后所返回的。
2. 如果 onFulfilled 或者 onRejectecd 缺失,那么不管它们所传递的是什么,Promise 通过 then() 返回出来。
显然,只需要调整下 then():
then(onFulfilled, onRejected) {
let returnValue = new Promise(); // (A)
let self = this;
let fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
let r = onFulfilled(self.promiseResult);
returnValue.resolve(r); // (B)
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult); // (C)
};
}
let rejectedTask;
if (typeof onRejected === 'function') {
rejectedTask = function () {
let r = onRejected(self.promiseResult);
returnValue.resolve(r); // (D)
};
} else {
rejectedTask = function () {
// `onRejected` has not been provided
// => we must pass on the rejection
returnValue.reject(self.promiseResult); // (E)
};
}
···
return returnValue; // (F)
}
then() 创建并返回一个新的Promise(在 A 处和 F 处)。此外,fulfilledTask 和 rejectedTask 以不同方式设置:在处理结束时。
1. onFulfilled 的结果被用于resolve returnValue(在 B 处)。如果 onFulfilled缺失,我们使用fulfillment值来resolve returnValue(在 C 处)。
2. onRejected 的结果被用于resolve(并非reject) returnValue(在 D 处)。如果onRejected 缺失,我们就传递rejection值给 returnValue(在 E 处)。
13.3 扁平化
扁平化(Flattening)思想主要是使链式方式更为便捷:一般,将一个 reaction 返回的值传递到下一个 then()。如果我们返回一个Promise,不包裹的形式是最好的,像下面的例子:
asyncFunc1()
.then(function (value1){
return asyncFunc2(); // A
})
.then(function (value2){
// value2 为 asyncFunc2() Promise的fulfillment值。
console.log(value2);
});
我们在 A 处返回一个Promise,就没有必要在当前方法中嵌套 then() 的调用,我们可以对方法的结果进行 then() 的调用。因此,不嵌套 then(),就保持扁平化。
下面的实现使 resolve() 方法扁平化:
1. 以一个Promise Q 来resolve 一个Promise P, 意味着 Q 的处理结果(settlement)要要转发给 P 的 reactions。
2. P变成被锁定在 Q 中:它不能被resolved(以及rejected)。并且它的状态和结果和 Q 的保持同步。
如果我们使Q成为then对象(而非一个Promise),便能以更通用的形式实现扁平化。
为实现锁定,我们需要使用一个新的布尔值标签 this.alreadyResolved。一旦值为 true,this 将被锁住,不能再被resolved。注意this状态仍然可以是pending,因为它的状态是和锁住它的Promise 是一致的。
resolve(value) {
if (this.alreadyResolved) return;
this.alreadyResolved = true;
this._doResolve(value);
return this; // enable chaining
}
真正实现解析是在私有方法 _doResolve() 中实现:
_doResolve(value) {
let self = this;
// Is `value` a thenable?
if (typeof value === 'object' && value !== null && 'then' in value) {
// Forward fulfillments and rejections from `value` to `this`.
// Added as a task (vs. done immediately) to preserve async semantics.
addToTaskQueue(function () { // (A)
value.then(
function onFulfilled(result) {
self._doResolve(result);
},
function onRejected(error) {
self._doReject(error);
});
});
} else {
this.promiseState = 'fulfilled';
this.promiseResult = value;
this._clearAndEnqueueReactions(this.fulfillReactions);
}
}
扁平化处理在 A 处进行:如果 value 为fulfilled状态,那么当前 self 为fulfilled状态,如果 value 为rejected状态,我们就希望 self 为rejected状态。转发是通过私有方法 _doResolve 和 _doReject 来做的,通过 alreadyResolved 来绕开阻挡。
13.4. 更详细的 Promise 状态
由于链式形式,Promises 的状态变得更为复杂。
如果你只是使用Promises,你只需要以简单的视角来看待,忽略锁定就好。最主要的状态相关概念是“settledness”: 一个Promise 状态如果变为fulfilled或者rejected的时候被设置完毕(is settled)。在一个Promise 被设置之后,将不再变化(状态和fullfillment或者rejection值都不再变)。
如果你希望实现Promsie,那么与"resolving"相关的内容,现在就更难理解:
1. 直观的说,‘resolved’(已处理)意味着不能再次被(直接)处理。当一个Promise 被设置或者被锁定时,那么就被称为resolved。引用下规范:一个未被处理的Promise总是处于pending 状态。一个已处理的Promise 可能是pending,fulfilled或者rejected状态。
2. 'resolving'(处理中)不一定会变为settling(被设置):你可能用状态总为pending的另一个Promise来resolve一个Promise。
3. 'resolving'处理中现在包含rejecting(拒绝):你可以用状态为rejected的另一个Promise来处理一个Promise,这样就可以reject这个Promise。
13.5. 异常
作为最后一个功能点,我们希望在 user code 中将异常处理为rejections。目前来说,user code 只代表 then 中的两个回调函数参数。
下面代码片段展现了我们将 onFulfilled 内部的异常变为rejections - 通过 A 处调用时的 try-catch 封装。
then(onFulfilled, onRejected) {
···
let fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
try {
let r = onFulfilled(self.promiseResult); // (A)
returnValue.resolve(r);
} catch (e) {
returnValue.reject(e);
}
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult);
};
}
···
}
13.6. 揭示构造函数模式
如果我们希望将 DemoPromise 转变为切实可用的Promise,还需要实现revealing constructor pattern(揭示构造函数模式):ES6 Promises 不是通过方法来resolved或者rejected,而是通过监听在构造函数的回调函数参数executor 上的函数实现的。
如果executor抛出一个异常,那么该Promise状态一定为rejected。
14. 两个常用的Promise 附加方法
本段介绍一下两个在 ES6 中添加到Promsie 的方法。大部分Promise 库都支持他们。
14.1. done()
当你将数个 Promise 方法链式调用时,你可能会无心之中忽略错误。看实例:
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.then(f2); // A
}
如果在 A 处的 then() 生成拒绝,那么将不会被处理。 Promise 库 Q 提供了一个方法 done(),放置在链式最后一个方法调用的后面。其或者替换最后一个 then()(有一个到两个参数):
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.done(f2);
}
或者仅仅是插到最后一个 then() 之后(无参数):
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done();
}
引用 Q 的文档:
done 与 then 使用的黄金规则:当返回一个Promise,或者结束链式调用,那么使用 done 来终止。使用 catch 来终止不是很好,因为 catch handler可能自己会抛出错误。
这也是在 ES6 中实现 done 的方式:
Promise.prototype.done = function(onFulfilled, onRejected){
this.then(onFulfilled, onRejected)
.catch(function(reason){
// Throw an exception globally
setTimeout( ()=>{ throw reason }, 0 );
});
};
虽然 done 功能极为有用,但是还没有添加到 ES6 中,因为在将来这种检测可以被引擎自动调用(在第12章中已经看到过)。
14.2. finally()
有时你希望不管是否出现错误,还是继续执行某个动作。例如在使用完一个资源后清理它。这便是Promise 方法 finally() 所做的,极为类似于异常处理机制中的 finally 。其回调方法不接受参数,但可以通过resolution或rejection来通知它。
createResource(···)
.then(function (value1) {
// Use resource
})
.then(function (value2) {
// Use resource
})
.finally(function () {
// Clean up
});
这是 Domenic Denicola 建议的finally()实现:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
// We don’t invoke the callback in here,
// because we want then() to handle its exceptions
return this.then(
// Callback fulfills => continue with receiver’s fulfillment or rejection
// Callback rejects => pass on that rejection (then() has no 2nd paramet\er!)
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
回调函数决定了 接收器(this)如何处理settlement:
1. 如果回调函数抛出一个异常或者返回一个rejected Promise,那么变为rejection值。
2. 否则,接收器的settlement (fulfillment 或 rejection)变为 finally( ) 返回的 promise 的settlement。通过这种方式从链式方法取出 finally() 的结果。
例一(by Jake Archibald):使用 finally() 来隐藏 spinner:
showSpinner();
fetchGalleryData()
.then(data => updateGallery(data) )
.catch(showNoDataError)
.finnally(hideSpinner);
例二(by Kris Kowal):使用 finally 来清除测试
let HTTP = require("q-io/http");
let server = HTTP.Server(app);
return server.listen(0)
.then(function () {
// run test
})
.finally(server.stop);
15. ES6 兼容的Promise 库
下面有一些Promise 库,下面这些库都遵循ES6 API,意味着你现在就可以使用它们,而且也容易迁移到将来的原生 ES6 代码。
"RSVP.js" 来自 Stefen Penner,为 ES6 promise API 的超集。
。“ES6-Promises”来自Jake Archibald, 从RSVP.js只提取出ES6 API。
"Native Promise Only (NPO)"来自 Kyle Simpson,是原生 ES6 promise 的 polyfill,尽可能接近而不是扩展严格规范定义。
"Lie" 来自 Calvin Metcalf, 小巧,完善的 promise 库,遵循 Promises/A+ 规范。
Q.promise 来自 Kris Kowal,实现 ES6 API。
最近,来自 Paul Millr 包含 promise 的 "ES6 Shim"。
16. 与传统异步代码交互
当你使用一个Promsie 库时,有时需要使用基于不支持Promsie 的异步代码。本章节讲述一下 Node.js 风格的异步函数与 jQuery deferreds。
16.1 与Node.js交互
Promise 库 Q 有几个工具方法,可以将使用 Node.js 风格(err, result)的回调的函数 转换 为返回Promise的函数(还有一些做相反动作的函数,将Promise函数转换为一些接受回调的函数)。例如:
let readFile = Q.denodeify(FS.readFile);
readFile('foo.txt', 'utf-8')
.then(function (text) {
···
});
denodify 为一个符合 ES6 Promise API ,仅提供 denodification 功能的一个微型库。
16.2. 与jQuery交互
jQuery 的 deferreds 类似于Promise,但是也有几个兼容性方面的不同。方法 then() 类似于ES6 Promises(主要不同之处是:不能捕获reactions 中的错误)。我们可以通过 Promise.resolve() 将 一个 jQuery deferred 转换为 一个ES6 Promise:
Promise.resolve(
jQuery.ajax({
url: 'somefile.html',
type: 'GET'
}))
.then(function (data) {
console.log(data);
})
.catch(function (reason) {
console.error(reason);
});
17. 延伸阅读
[1] "Promises/A+"(http://promisesaplus.com/), Brian Cavalier 与 Domenic Denicola 编辑(JS Promises事实标准)
[2] "Javascript Promises:再次强势归来"(http://www.html5rocks.com/en/tutorials/es6/promises/), 来自 Jake Archibad(挺不错的 promise 简要介绍)
[3] "Promsie 反模式"(http://taoofcode.net/promise-anti-patterns/), 来自代码之道(要点与技术)
[4] "Promise 模式"(https://www.promisejs.org/patterns/)来自 Forbes Lindeasy
[5] "Revealing构造函数模式"(http://domenic.me/2014/02/13/the-revealing-constructor-pattern/), 来自 Domenic Denicola(为 Promise 构造函数使用的模式)
来源:oschina
链接:https://my.oschina.net/u/2288810/blog/538609