本章介绍如何通过 Promise 进行异步编程,特别是 ECMAScript 6 Promise API。 上一章 解释了 JavaScript 中异步编程的基础知识。 如果您在本章中遇到任何不理解的内容,可以参考上一章。
then()fs.readFile() Promise 化XMLHttpRequest Promise 化Promise.resolve()Promise.reject()onRejected 解析 Qthen() 进行错误处理Promise.all() 分叉和合并计算Promise.all() 实现 map()Promise.race() 实现超时done()finally()Promise 构造函数Promise 方法Promise.prototype 方法Promise 是回调函数的替代方案,用于传递异步计算的结果。 它们需要异步函数的实现者付出更多努力,但为这些函数的用户提供了许多好处。
以下函数通过 Promise 异步返回结果
function asyncFunc() {
return new Promise(
function (resolve, reject) {
···
resolve(result);
···
reject(error);
});
}
您可以按如下方式调用 asyncFunc()
asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });
then() then() 始终返回一个 Promise,这使您可以链式调用方法
asyncFunc1()
.then(result1 => {
// Use result1
return asyncFunction2(); // (A)
})
.then(result2 => { // (B)
// Use result2
})
.catch(error => {
// Handle errors of asyncFunc1() and asyncFunc2()
});
then() 返回的 Promise P 如何解决取决于其回调函数的操作
asyncFunction2 的 Promise 的解决结果。此外,请注意 catch() 如何处理两个异步函数调用(asyncFunction1() 和 asyncFunction2())的错误。 也就是说,未捕获的错误会一直传递,直到遇到错误处理程序。
如果通过 then() 链式调用异步函数,则它们将按顺序执行,一次执行一个
asyncFunc1()
.then(() => asyncFunc2());
如果您不这样做,而是立即调用所有函数,则它们基本上是并行执行的(在 Unix 进程术语中称为“分叉”)
asyncFunc1();
asyncFunc2();
Promise.all() 使您能够在所有结果都返回时收到通知(在 Unix 进程术语中称为“合并”)。 它的输入是一个 Promise 数组,输出是一个 Promise,该 Promise 将使用结果数组来完成。
Promise.all([
asyncFunc1(),
asyncFunc2(),
])
.then(([result1, result2]) => {
···
})
.catch(err => {
// Receives first rejection among the Promises
···
});
Promise API 用于异步传递结果。 *Promise 对象*(简称:Promise)是结果的占位符和容器,结果将通过该对象传递。
状态
对状态更改做出反应
then() 注册的回调函数,用于在完成或拒绝时收到通知。then() 方法的对象。 每当 API 只想收到解决通知时,它只需要 thenable(例如,从 then() 和 catch() 返回的值;或传递给 Promise.all() 和 Promise.race() 的值)。更改状态:有两个操作可以更改 Promise 的状态。 在您调用其中任何一个操作一次后,进一步的调用将无效。
Promise 是一种模式,有助于处理一种特定类型的异步编程:异步返回单个结果的函数(或方法)。 接收此类结果的一种常用方法是通过回调(“回调作为延续”)
asyncFunction(arg1, arg2,
result => {
console.log(result);
});
Promise 提供了一种更好的使用回调函数的方法:现在,异步函数返回一个 *Promise*,这是一个充当最终结果的占位符和容器的对象。 通过 Promise 方法 then() 注册的回调函数将在结果返回时收到通知
asyncFunction(arg1, arg2)
.then(result => {
console.log(result);
});
与回调作为延续相比,Promise 具有以下优点
then() 的回调函数返回一个 Promise(例如,调用另一个基于 Promise 的函数的结果),则 then() 返回该 Promise(这实际上是如何工作的更加复杂,将在后面解释)。 因此,您可以链式调用 then() 方法 asyncFunction1(a, b)
.then(result1 => {
console.log(result1);
return asyncFunction2(x, y);
})
.then(result2 => {
console.log(result2);
});
让我们看第一个例子,让您体验一下使用 Promises 是什么样的。
使用 Node.js 风格的回调函数,异步读取文件如下所示
fs.readFile('config.json',
function (error, text) {
if (error) {
console.error('Error while reading config file');
} else {
try {
const obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
} catch (e) {
console.error('Invalid JSON in file');
}
}
});
使用 Promises,相同的功能的使用方式如下
readFilePromisified('config.json')
.then(function (text) { // (A)
const obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
})
.catch(function (error) { // (B)
// File read error or JSON SyntaxError
console.error('An error occurred', error);
});
仍然有回调函数,但它们是通过在结果上调用的方法提供的(then() 和 catch())。 B 行中的错误回调函数在两个方面很方便:首先,它是一种单一的错误处理风格(与上一个例子中的 if (error) 和 try-catch 相比)。 其次,您可以从一个位置处理 readFilePromisified() 和 A 行中回调函数的错误。
readFilePromisified() 的代码 稍后显示。
让我们看看理解 Promises 的三种方式。
以下代码包含一个基于 Promise 的函数 asyncFunc() 及其调用。
function asyncFunc() {
return new Promise((resolve, reject) => { // (A)
setTimeout(() => resolve('DONE'), 100); // (B)
});
}
asyncFunc()
.then(x => console.log('Result: '+x));
// Output:
// Result: DONE
asyncFunc() 返回一个 Promise。 一旦异步计算的实际结果 'DONE' 准备就绪,它将通过 resolve()(B 行)传递,resolve() 是 A 行中启动的回调函数的参数。
那么 Promise 是什么呢?
asyncFunc() 是一个阻塞函数调用。以下代码从异步函数 main() 调用 asyncFunc()。 异步函数 是 ECMAScript 2017 的一个特性。
async function main() {
const x = await asyncFunc(); // (A)
console.log('Result: '+x); // (B)
// Same as:
// asyncFunc()
// .then(x => console.log('Result: '+x));
}
main();
main() 的主体很好地表达了 *概念上* 发生的事情,即我们通常如何看待异步计算。 也就是说,asyncFunc() 是一个阻塞函数调用
asyncFunc() 完成。x。在 ECMAScript 6 和生成器之前,您无法挂起和恢复代码。 这就是为什么对于 Promises,您将代码恢复后发生的所有事情都放入回调函数中。 调用该回调函数与恢复代码相同。
如果一个函数返回一个 Promise,那么这个 Promise 就像一个空白容器,函数会在计算出结果后(通常情况下)将结果填充进去。你可以通过数组模拟这个过程的简单版本。
function asyncFunc() {
const blank = [];
setTimeout(() => blank.push('DONE'), 100);
return blank;
}
const blank = asyncFunc();
// Wait until the value has been filled in
setTimeout(() => {
const x = blank[0]; // (A)
console.log('Result: '+x);
}, 200);
使用 Promise 时,你不需要通过 [0](如代码行 A 所示)访问最终值,而是使用 then() 方法和一个回调函数。
另一种看待 Promise 的方式是将其视为一个发出事件的对象。
function asyncFunc() {
const eventEmitter = { success: [] };
setTimeout(() => { // (A)
for (const handler of eventEmitter.success) {
handler('DONE');
}
}, 100);
return eventEmitter;
}
asyncFunc()
.success.push(x => console.log('Result: '+x)); // (B)
注册事件监听器(代码行 B)可以在调用 asyncFunc() 之后完成,因为传递给 setTimeout() 的回调函数(代码行 A)是异步执行的(在这段代码执行完毕之后)。
普通的事件发射器擅长于传递多个事件,并在注册后立即开始传递。
相比之下,Promise 擅长于只传递一个值,并且内置了防止注册过晚的保护机制:Promise 的结果会被缓存,并传递给 Promise 兑现后注册的事件监听器。
让我们来看看如何在生产者和消费者端操作 Promise。
作为生产者,你需要创建一个 Promise 并通过它发送结果。
const p = new Promise(
function (resolve, reject) { // (A)
···
if (···) {
resolve(value); // success
} else {
reject(reason); // failure
}
});
一旦结果通过 Promise 传递,Promise 就会锁定该结果。这意味着每个 Promise 始终处于以下三种(互斥)状态之一:
如果 Promise 处于已兑现或已拒绝状态,则称其为已 *兑现*(它所代表的计算已完成)。一个 Promise 只能兑现一次,并且一旦兑现就会保持该状态。后续尝试兑现操作将无效。
new Promise() 的参数(从代码行 A 开始)称为 *执行器*。
resolve() 发送结果。这通常会兑现 Promise p。但也有可能不会——使用 Promise q 进行兑现会导致 p 跟踪 q:如果 q 仍处于待定状态,则 p 也处于待定状态。然而,无论 q 如何兑现,p 都会以相同的方式兑现。reject() 通知 Promise 消费者。这将始终拒绝 Promise。如果在执行器内部抛出异常,则 p 将以该异常被拒绝。
作为 promise 的消费者,你会通过 *反应* 被告知兑现或拒绝——你使用 then() 和 catch() 方法注册的回调函数。
promise
.then(value => { /* fulfillment */ })
.catch(error => { /* rejection */ });
Promise 之所以对异步函数(具有一次性结果)如此有用,是因为一旦 Promise 兑现,它就不会再改变。此外,永远不会出现竞争条件,因为无论是在 Promise 兑现之前还是之后调用 then() 或 catch() 都无关紧要。
请注意,catch() 只是调用 then() 的一种更方便(也是推荐的)替代方法。也就是说,以下两个调用是等效的:
promise.then(
null,
error => { /* rejection */ });
promise.catch(
error => { /* rejection */ });
Promise 库可以完全控制结果是同步(立即)还是异步(在当前延续,即当前代码段完成后)传递给 Promise 反应。但是,Promises/A+ 规范要求始终使用后一种执行模式。它通过 then() 方法的以下要求(2.2.4)来规定这一点:
在执行上下文堆栈仅包含平台代码之前,不得调用
onFulfilled或onRejected。
这意味着你的代码可以依赖于运行至完成语义(如上一章所述),并且 Promise 链不会使其他任务无法获得处理时间。
此外,此约束还阻止你编写有时立即返回结果、有时异步返回结果的函数。这是一种反模式,因为它会使代码变得不可预测。有关更多信息,请参阅 Isaac Z. Schlueter 的“为异步设计 API”。
在深入探讨 Promise 之前,让我们在几个示例中使用到目前为止所学到的知识。
fs.readFile() Promise 化 以下代码是内置 Node.js 函数 fs.readFile() 的基于 Promise 的版本。
import {readFile} from 'fs';
function readFilePromisified(filename) {
return new Promise(
function (resolve, reject) {
readFile(filename, { encoding: 'utf8' },
(error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
readFilePromisified() 的使用方法如下:
readFilePromisified(process.argv[2])
.then(text => {
console.log(text);
})
.catch(error => {
console.log(error);
});
XMLHttpRequest Promise 化 以下是一个基于 Promise 的函数,它通过基于事件的 XMLHttpRequest API 执行 HTTP GET 请求。
function httpGet(url) {
return new Promise(
function (resolve, reject) {
const request = new XMLHttpRequest();
request.onload = function () {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
};
request.onerror = function () {
reject(new Error(
'XMLHttpRequest Error: '+this.statusText));
};
request.open('GET', url);
request.send();
});
}
以下是使用 httpGet() 的方法:
httpGet('http://example.com/file.txt')
.then(
function (value) {
console.log('Contents: ' + value);
},
function (reason) {
console.error('Something went wrong', reason);
});
让我们将 setTimeout() 实现为基于 Promise 的函数 delay()(类似于 Q.delay())。
function delay(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms); // (A)
});
}
// Using delay():
delay(5000).then(function () { // (B)
console.log('5 seconds have passed!')
});
请注意,在代码行 A 中,我们使用零个参数调用 resolve,这与调用 resolve(undefined) 相同。我们在代码行 B 中也不需要兑现值,只需忽略它即可。在这里,只需收到通知就足够了。
function timeout(ms, promise) {
return new Promise(function (resolve, reject) {
promise.then(resolve);
setTimeout(function () {
reject(new Error('Timeout after '+ms+' ms')); // (A)
}, ms);
});
}
请注意,超时后的拒绝(代码行 A)不会取消请求,但会阻止 Promise 以其结果兑现。
使用 timeout() 的方法如下:
timeout(5000, httpGet('http://example.com/file.txt'))
.then(function (value) {
console.log('Contents: ' + value);
})
.catch(function (reason) {
console.error('Error or timeout', reason);
});
现在我们准备深入探讨 Promise 的特性。让我们先来探讨另外两种创建 Promise 的方法。
Promise.resolve() Promise.resolve(x) 的工作原理如下:
x,它返回一个以 x 兑现的 Promise。 Promise.resolve('abc')
.then(x => console.log(x)); // abc
x 是一个构造函数为接收器(如果调用 Promise.resolve() 则为 Promise)的 Promise,则返回 x 本身。 const p = new Promise(() => null);
console.log(Promise.resolve(p) === p); // true
x 是一个 thenable 对象,则将其转换为 Promise:thenable 对象的兑现也将成为 Promise 的兑现。以下代码演示了这一点。fulfilledThenable 的行为大致类似于以字符串 'hello' 兑现的 Promise。将其转换为 Promise promise 后,then() 方法按预期工作(最后一行)。 const fulfilledThenable = {
then(reaction) {
reaction('hello');
}
};
const promise = Promise.resolve(fulfilledThenable);
console.log(promise instanceof Promise); // true
promise.then(x => console.log(x)); // hello
这意味着你可以使用 Promise.resolve() 将任何值(Promise、thenable 对象或其他值)转换为 Promise。实际上,Promise.all() 和 Promise.race() 使用它将任意值的数组转换为 Promise 数组。
Promise.reject() Promise.reject(err) 返回一个以 err 拒绝的 Promise。
const myError = new Error('Problem!');
Promise.reject(myError)
.catch(err => console.log(err === myError)); // true
在本节中,我们将仔细研究如何链接 Promise。方法调用的结果
P.then(onFulfilled, onRejected)
是一个新的 Promise Q。这意味着你可以通过在 Q 上调用 then() 来保持基于 Promise 的控制流。
onFulfilled 或 onRejected 返回的值兑现。onFulfilled 或 onRejected 抛出异常,则 Q 将被拒绝。如果你使用普通值兑现 then() 返回的 Promise Q,则可以通过后续的 then() 获取该值。
asyncFunc()
.then(function (value1) {
return 123;
})
.then(function (value2) {
console.log(value2); // 123
});
你也可以使用 *thenable 对象* R 兑现 then() 返回的 Promise Q。thenable 对象是任何具有 then() 方法的对象,该方法的工作原理类似于 Promise.prototype.then()。因此,Promise 是 thenable 对象。使用 R 兑现(例如,通过从 onFulfilled 返回它)意味着它被插入到 Q 的“后面”:R 的兑现将转发给 Q 的 onFulfilled 和 onRejected 回调函数。在某种程度上,Q 变成了 R。
此机制的主要用途是扁平化嵌套的 then() 调用,如下例所示:
asyncFunc1()
.then(function (value1) {
asyncFunc2()
.then(function (value2) {
···
});
})
扁平化版本如下所示:
asyncFunc1()
.then(function (value1) {
return asyncFunc2();
})
.then(function (value2) {
···
})
onRejected 兑现 Q 你在错误处理程序中返回的任何内容都将成为兑现值(而不是拒绝值!)。这允许你指定在发生故障时使用的默认值。
retrieveFileName()
.catch(function () {
// Something went wrong, use a default value
return 'Untitled.txt';
})
.then(function (fileName) {
···
});
在 then() 和 catch() 的回调函数中抛出的异常将作为拒绝传递给下一个错误处理程序。
asyncFunc()
.then(function (value) {
throw new Error();
})
.catch(function (reason) {
// Handle error here
});
可能存在一个或多个没有错误处理程序的 then() 方法调用。然后,错误将一直传递,直到遇到错误处理程序为止。
asyncFunc1()
.then(asyncFunc2)
.then(asyncFunc3)
.catch(function (reason) {
// Something went wrong above
});
在以下代码中,构建了一个由两个 Promise 组成的链,但只返回了链的第一部分。结果,链的尾部丢失了。
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
···
});
return promise;
}
可以通过返回链的尾部来解决此问题:
function foo() {
const promise = asyncFunc();
return promise.then(result => {
···
});
}
如果你不需要变量 promise,则可以进一步简化此代码:
function foo() {
return asyncFunc()
.then(result => {
···
});
}
在以下代码中,asyncFunc2() 的调用是嵌套的:
// Don’t do this
asyncFunc1()
.then(result1 => {
asyncFunc2()
.then(result2 => {
···
});
});
解决方法是通过从第一个 then() 返回第二个 Promise 并通过第二个链接的 then() 处理它来取消嵌套此代码:
asyncFunc1()
.then(result1 => {
return asyncFunc2();
})
.then(result2 => {
···
});
在以下代码中,方法 insertInto() 为其结果创建了一个新的 Promise(代码行 A):
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
db.insert(this.fields) // (B)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
resolve(resultCode); // (C)
}).catch(err => {
reject(err); // (D)
})
});
}
···
}
如果仔细观察,你会发现结果 Promise 主要用于转发异步方法调用 db.insert() 的兑现(代码行 C)和拒绝(代码行 D)。
解决方法是不创建 Promise,而是依赖 then() 和链接:
class Model {
insertInto(db) {
return db.insert(this.fields) // (A)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
return resultCode; // (B)
});
}
···
}
说明:
resultCode(代码行 B),并让 then() 为我们创建 Promise。then() 将传递 db.insert() 生成的任何拒绝。then() 进行错误处理 原则上,catch(cb) 是 then(null, cb) 的缩写。但是同时使用 then() 的两个参数可能会导致问题。
// Don’t do this
asyncFunc1()
.then(
value => { // (A)
doSomething(); // (B)
return asyncFunc2(); // (C)
},
error => { // (D)
···
});
拒绝回调(D 行)接收 asyncFunc1() 的所有拒绝,但它不接收由完成回调(A 行)创建的拒绝。例如,B 行中的同步函数调用可能会抛出异常,或者 C 行中的异步函数调用可能会产生拒绝。
因此,最好将拒绝回调移动到链式 catch() 中。
asyncFunc1()
.then(value => {
doSomething();
return asyncFunc2();
})
.catch(error => {
···
});
在程序中,有两种错误:
对于操作错误,每个函数应该只支持一种错误信号方式。对于基于 Promise 的函数,这意味着不要混淆拒绝和异常,也就是说它们不应该抛出异常。
对于程序员错误,可以通过抛出异常来尽快失败。
function downloadFile(url) {
if (typeof url !== 'string') {
throw new Error('Illegal argument: ' + url);
}
return new Promise(···).
}
如果这样做,则必须确保异步代码可以处理异常。我发现对于断言和理论上可以静态检查(例如,通过分析源代码的 linter)的类似情况,抛出异常是可以接受的。
如果在 then() 和 catch() 的回调中抛出异常,则这不是问题,因为这两个方法会将它们转换为拒绝。
但是,如果您通过执行同步操作来启动异步函数,情况就会有所不同。
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
···
});
}
如果在 A 行中抛出异常,则整个函数都会抛出异常。这个问题有两个解决方案。
您可以捕获异常并将其作为被拒绝的 Promise 返回。
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
···
});
} catch (err) {
return Promise.reject(err);
}
}
您也可以通过 Promise.resolve() 启动 then() 方法调用链,并在回调中执行同步代码。
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
···
});
}
另一种方法是通过 Promise 构造函数启动 Promise 链。
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
···
});
}
这种方法可以为您节省一个计时周期(同步代码会立即执行),但会降低代码的规范性。
本节的参考资料:
组合意味着从现有部分创建新事物。我们已经遇到了 Promise 的顺序组合:给定两个 Promise P 和 Q,以下代码生成一个新的 Promise,在 P 完成后执行 Q。
P.then(() => Q)
请注意,这类似于同步代码的分号:同步操作 f() 和 g() 的顺序组合如下所示。
f(); g()
本节介绍组合 Promise 的其他方法。
假设您要并行执行两个异步计算,asyncFunc1() 和 asyncFunc2()。
// Don’t do this
asyncFunc1()
.then(result1 => {
handleSuccess({result1});
});
.catch(handleError);
asyncFunc2()
.then(result2 => {
handleSuccess({result2});
})
.catch(handleError);
const results = {};
function handleSuccess(props) {
Object.assign(results, props);
if (Object.keys(results).length === 2) {
const {result1, result2} = results;
···
}
}
let errorCounter = 0;
function handleError(err) {
errorCounter++;
if (errorCounter === 1) {
// One error means that everything failed,
// only react to first error
···
}
}
这两个函数调用 asyncFunc1() 和 asyncFunc2() 是在没有 then() 链的情况下进行的。因此,它们都会立即执行,并且或多或少是并行的。现在执行已分叉;每个函数调用都产生了一个单独的“线程”。一旦两个线程都完成(有结果或错误),执行就会在 handleSuccess() 或 handleError() 中连接到单个线程。
这种方法的问题在于它涉及太多手动且容易出错的工作。解决方法是不要自己做这件事,而是依靠内置方法 Promise.all()。
Promise.all() 分叉和连接计算 Promise.all(iterable) 接受一个 Promise 的可迭代对象(thenable 和其他值通过 Promise.resolve() 转换为 Promise)。一旦所有 Promise 都已完成,它就会使用其值的数组来完成。如果 iterable 为空,则 all() 返回的 Promise 会立即完成。
Promise.all([
asyncFunc1(),
asyncFunc2(),
])
.then(([result1, result2]) => {
···
})
.catch(err => {
// Receives first rejection among the Promises
···
});
Promise.all() 实现 map() Promise 的一个好处是,许多同步工具仍然有效,因为基于 Promise 的函数会返回结果。例如,您可以使用数组方法 map()。
const fileUrls = [
'http://example.com/file1.txt',
'http://example.com/file2.txt',
];
const promisedTexts = fileUrls.map(httpGet);
promisedTexts 是一个 Promise 数组。我们可以使用上一节中已经介绍过的 Promise.all() 将该数组转换为一个 Promise,该 Promise 使用结果数组来完成。
Promise.all(promisedTexts)
.then(texts => {
for (const text of texts) {
console.log(text);
}
})
.catch(reason => {
// Receives first rejection among the Promises
});
Promise.race() 实现超时 Promise.race(iterable) 接受一个 Promise 的可迭代对象(thenable 和其他值通过 Promise.resolve() 转换为 Promise),并返回一个 Promise P。第一个完成的输入 Promise 会将其完成状态传递给输出 Promise。如果 iterable 为空,则 race() 返回的 Promise 永远不会完成。
例如,让我们使用 Promise.race() 来实现超时。
Promise.race([
httpGet('http://example.com/file.txt'),
delay(5000).then(function () {
throw new Error('Timed out')
});
])
.then(function (text) { ··· })
.catch(function (reason) { ··· });
本节介绍许多 Promise 库提供的两个有用的 Promise 方法。它们只是为了进一步演示 Promise,您不应该将它们添加到 Promise.prototype 中(这种类型的修补应该只由 polyfill 完成)。
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 处理程序本身可能会抛出错误。
这就是您在 ECMAScript 6 中实现 done() 的方式:
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// Throw an exception globally
setTimeout(() => { throw reason }, 0);
});
};
虽然 done 的功能显然很有用,但它还没有被添加到 ECMAScript 6 中。其想法是首先探索引擎可以自动检测到多少。根据其工作情况,可能需要引入 done()。
finally() 有时,您希望执行一个操作,而不管是否发生错误。例如,在您使用完资源后进行清理。这就是 Promise 方法 finally() 的用途,它的工作方式与异常处理中的 finally 子句非常相似。它的回调不接收任何参数,但会被通知完成或拒绝。
createResource(···)
.then(function (value1) {
// Use resource
})
.then(function (value2) {
// Use resource
})
.finally(function () {
// Clean up
});
这就是 Domenic Denicola 建议实现 finally() 的方式:
Promise.prototype.finally = function (callback) {
const 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 rejec\
tion
// Callback rejects => pass on that rejection (then() has no 2nd para\
meter!)
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
回调确定如何处理接收方(this)的完成状态:
finally() 返回的 Promise 的完成状态。在某种程度上,我们将 finally() 从方法链中移除了。**示例 1**(作者:Jake Archibald):使用 finally() 隐藏微调器。简化版本:
showSpinner();
fetchGalleryData()
.then(data => updateGallery(data))
.catch(showNoDataError)
.finally(hideSpinner);
**示例 2**(作者:Kris Kowal):使用 finally() 拆除测试。
const HTTP = require("q-io/http");
const server = HTTP.Server(app);
return server.listen(0)
.then(function () {
// run test
})
.finally(server.stop);
Promise 库 Q 具有用于与 Node.js 风格的 (err, result) 回调 API 交互的工具函数。例如,denodeify 将基于回调的函数转换为基于 Promise 的函数。
const readFile = Q.denodeify(FS.readFile);
readFile('foo.txt', 'utf-8')
.then(function (text) {
···
});
denodify 是一个只提供 Q.denodeify() 功能并符合 ECMAScript 6 Promise API 的微型库。
市面上有很多 Promise 库。以下库符合 ECMAScript 6 API,这意味着您可以现在就使用它们,并在以后轻松迁移到原生 ES6。
最小 polyfill:
更大的 Promise 库:
Q.Promise实现了 ES6 API。ES6 标准库 polyfill:
通过 Promise 实现异步函数比通过事件或回调更方便,但它仍然不理想。
解决方案是将阻塞调用引入 JavaScript。生成器让我们可以通过库来做到这一点:在下面的代码中,我使用 控制流库 co 来异步检索两个 JSON 文件。
co(function* () {
try {
const [croftStr, bondStr] = yield Promise.all([ // (A)
getFile('https://:8000/croft.json'),
getFile('https://:8000/bond.json'),
]);
const croftJson = JSON.parse(croftStr);
const bondJson = JSON.parse(bondStr);
console.log(croftJson);
console.log(bondJson);
} catch (e) {
console.log('Failure to read: ' + e);
}
});
在 A 行,执行通过 yield 阻塞(等待),直到 Promise.all() 的结果准备就绪。这意味着代码在执行异步操作时看起来是同步的。
详细信息在关于生成器的章节中解释。
在本节中,我们将从不同的角度来探讨 Promise:我们不是学习如何使用 API,而是查看它的一个简单实现。这个不同的角度极大地帮助我理解了 Promise。
Promise 实现称为 DemoPromise。为了更容易理解,它与 API 并不完全匹配。但它已经足够接近,仍然可以让你深入了解实际实现所面临的挑战。
DemoPromise 是一个具有三个原型方法的类
DemoPromise.prototype.resolve(value)DemoPromise.prototype.reject(reason)DemoPromise.prototype.then(onFulfilled, onRejected)也就是说,resolve 和 reject 是方法(而不是传递给构造函数的回调参数的函数)。
我们的第一个实现是一个具有最小功能的独立 Promise
then() 注册*反应*(回调)。无论 Promise 是否已经解决,它都必须独立工作。以下是第一个实现的使用方式
const dp = new DemoPromise();
dp.resolve('abc');
dp.then(function (value) {
console.log(value); // abc
});
下图说明了我们的第一个 DemoPromise 的工作原理
DemoPromise.prototype.then() 让我们先检查 then()。它必须处理两种情况
onFulfilled 和 onRejected 的调用排队,以便在 Promise 解决时使用。onFulfilled 或 onRejected。then(onFulfilled, onRejected) {
const self = this;
const fulfilledTask = function () {
onFulfilled(self.promiseResult);
};
const 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);
}
DemoPromise.prototype.resolve() resolve() 的工作原理如下:如果 Promise 已经解决,它什么也不做(确保 Promise 只能解决一次)。否则,Promise 的状态变为 'fulfilled',结果缓存在 this.promiseResult 中。接下来,触发到目前为止已排队的全部完成反应。
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()。
我们要实现的下一个功能是链接
then() 返回一个 Promise,该 Promise 将使用 onFulfilled 或 onRejected 返回的内容来解决。onFulfilled 或 onRejected,则它们将收到的任何内容都将传递给 then() 返回的 Promise。
显然,只有 then() 会改变
then(onFulfilled, onRejected) {
const returnValue = new Promise(); // (A)
const self = this;
let fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
const r = onFulfilled(self.promiseResult);
returnValue.resolve(r); // (B)
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult); // (C)
};
}
let rejectedTask;
if (typeof onRejected === 'function') {
rejectedTask = function () {
const 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 的设置方式不同:在解决后…
onFulfilled 的结果用于解决 returnValue(B 行)。onFulfilled,我们将使用完成值来解决 returnValue(C 行)。onRejected 的结果用于解决(而不是拒绝!)returnValue(D 行)。onRejected,我们将拒绝值传递给 returnValue(E 行)。扁平化主要是为了使链接更方便:通常,从反应中返回值会将其传递给下一个 then()。如果我们返回一个 Promise,如果它可以为我们“解包”,就像在下面的例子中那样,那就太好了
asyncFunc1()
.then(function (value1) {
return asyncFunc2(); // (A)
})
.then(function (value2) {
// value2 is fulfillment value of asyncFunc2() Promise
console.log(value2);
});
我们在 A 行返回了一个 Promise,并且不必在当前方法中嵌套调用 then(),我们可以调用方法结果上的 then()。因此:没有嵌套的 then(),一切都保持扁平。
我们通过让 resolve() 方法进行扁平化来实现这一点
如果我们允许 Q 是可 thenable 的(而不仅仅是一个 Promise),我们可以使扁平化更通用。
为了实现锁定,我们引入了一个新的布尔标志 this.alreadyResolved。一旦它为真,this 就被锁定并且不能再被解决。请注意,this 可能仍在等待中,因为它现在的状态与其锁定的 Promise 相同。
resolve(value) {
if (this.alreadyResolved) return;
this.alreadyResolved = true;
this._doResolve(value);
return this; // enable chaining
}
实际的解决现在发生在私有方法 _doResolve() 中
_doResolve(value) {
const 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 (versus done immediately) to preserve async semant\
ics.
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 完成,我们希望 self 完成,如果 value 被拒绝,我们希望 self 被拒绝。转发通过私有方法 _doResolve 和 _doReject 进行,以绕过 alreadyResolved 的保护。
通过链接,Promise 的状态变得更加复杂(如 ECMAScript 6 规范的 第 25.4 节 所述)
如果你只是*使用* Promise,你通常可以采用简化的世界观并忽略锁定。最重要的状态相关概念仍然是“已解决”:如果 Promise 已完成或已拒绝,则表示已解决。Promise 解决后,它就不会再改变了(状态和完成或拒绝值)。
如果你想*实现* Promise,那么“解决”也很重要,现在更难理解了
作为我们的最后一个功能,我们希望我们的 Promise 将用户代码中的异常作为拒绝处理。目前,“用户代码”指的是 then() 的两个回调参数。
以下摘录显示了我们如何将 onFulfilled 内部的异常转换为拒绝 - 通过在其调用周围包装一个 try-catch(A 行)。
then(onFulfilled, onRejected) {
···
let fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
try {
const r = onFulfilled(self.promiseResult); // (A)
returnValue.resolve(r);
} catch (e) {
returnValue.reject(e);
}
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult);
};
}
···
}
如果我们想将 DemoPromise 变成一个实际的 Promise 实现,我们仍然需要实现 揭示构造函数模式 [2]:ES6 Promise 不是通过方法解决和拒绝的,而是通过传递给*执行器*(构造函数的回调参数)的函数来解决和拒绝的。
如果执行器抛出异常,则必须拒绝“其”Promise。
Promise 的一个重要优势是它们将越来越多地被异步浏览器 API 使用,并统一当前不同且不兼容的模式和约定。让我们来看看两个即将推出的基于 Promise 的 API。
Fetch API 是 XMLHttpRequest 的基于 Promise 的替代方案
fetch(url)
.then(request => request.text())
.then(str => ···)
fetch() 返回实际请求的 Promise,text() 返回内容作为字符串的 Promise。
用于以编程方式导入模块的 ECMAScript 6 API 也基于 Promise
System.import('some_module.js')
.then(some_module => {
···
})
与事件相比,Promise 更适合处理一次性结果。无论是在计算结果之前还是之后注册结果,你都将获得结果。Promise 的这一优势是本质上的。另一方面,你不能将它们用于处理重复事件。链接是 Promise 的另一个优势,但可以将其添加到事件处理中。
与回调相比,Promise 具有更清晰的函数(或方法)签名。对于回调,参数用于输入和输出
fs.readFile(name, opts?, (err, string | Buffer) => void)
对于 Promise,所有参数都用于输入
readFilePromisified(name, opts?) : Promise<string | Buffer>
Promise 的其他优势包括
Array.prototype.map()。then() 和 catch() 的链接。Promise 适用于单个异步结果。它们不适合
ECMAScript 6 Promise 缺少两个有时很有用的功能
Q Promise 库 支持 后者,并且有 计划 将这两种功能添加到 Promises/A+ 中。
本节概述了 ECMAScript 6 Promise API,如 规范 中所述。
Promise 构造函数 Promise 的构造函数按如下方式调用
const p = new Promise(function (resolve, reject) { ··· });
此构造函数的回调称为*执行器*。执行器可以使用其参数来解决或拒绝新的 Promise p
resolve(x) 使用 x 解决 px 是可 thenable 的,则其解决将转发给 p(包括触发通过 then() 注册的反应)。p 将使用 x 完成。reject(e) 使用值 e(通常是 Error 的实例)拒绝 p。Promise 方法 以下两个静态方法创建其接收者的新实例
Promise.resolve(x):将任意值转换为 Promise,并意识到 Promise。x 的构造函数是接收者,则 x 不变地返回。x 完成。Promise.reject(reason):创建一个新的接收器实例,该实例以值 reason 拒绝。直观地说,静态方法 Promise.all() 和 Promise.race() 将 Promise 的可迭代对象组合成单个 Promise。 也就是说
this.resolve() 转换为 Promise。这些方法是
Promise.all(iterable):返回一个 Promise,该 Promise…iterable 中的所有元素都已完成,则完成。Promise.race(iterable):iterable 中第一个已决定的元素用于决定返回的 Promise。Promise.prototype 方法 Promise.prototype.then(onFulfilled, onRejected) onFulfilled 和 onRejected 被称为 *反应*。onFulfilled,或者一旦完成就立即调用。 同样,onRejected 会被告知拒绝。then() 返回一个新的 Promise Q(通过接收器的构造函数的种类创建)onFulfilled,则接收器的完成将转发到 then() 的结果。onRejected,则接收器的拒绝将转发到 then() 的结果。省略的反应的默认值可以像这样实现
function defaultOnFulfilled(x) {
return x;
}
function defaultOnRejected(e) {
throw e;
}
Promise.prototype.catch(onRejected) p.catch(onRejected) 与 p.then(null, onRejected) 相同。[1] “Promises/A+”,由 Brian Cavalier 和 Domenic Denicola 编辑(JavaScript Promise 的事实标准)
[2] “揭示构造函数模式”,作者 Domenic Denicola(Promise 构造函数使用此模式)