Javascript.info-Promise-async-await
Promise 对象的构造器(constructor)语法如下:
Promise 对象的构造器(constructor)语法如下:
let promise = new Promise(function(resolve, reject) {
// executor(生产者代码,“歌手”)
});传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。
它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。
当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:
resolve(value)—— 如果任务成功完成并带有结果value。reject(error)—— 如果出现了 error,error即为 error 对象。
所以总结一下就是:executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。
由 new Promise 构造器返回的 promise 对象具有以下内部属性:
state—— 最初是"pending",然后在resolve被调用时变为"fulfilled",或者在reject被调用时变为"rejected"。result—— 最初是undefined,然后在resolve(value)被调用时变为value,或者在reject(error)被调用时变为error。
所以,executor 最终将 promise 移至以下状态之一:

下面是一个 promise 构造器和一个简单的 executor 函数,该 executor 函数具有包含时间(即 setTimeout)的“生产者代码”:
let promise = new Promise(function(resolve, reject) {
// 当 promise 被构造完成时,自动执行此函数
// 1 秒后发出工作已经被完成的信号,并带有结果 "done"
setTimeout(() => resolve("done"), 1000);
});通过运行上面的代码,我们可以看到两件事儿:
-
executor 被自动且立即调用(通过
new Promise)。 -
executor 接受两个参数:
resolve和reject。这些函数由 JavaScript 引擎预先定义,因此我们不需要创建它们。我们只需要在准备好(译注:指的是 executor 准备好)时调用其中之一即可。经过 1 秒的“处理”后,executor 调用
resolve("done")来产生结果。这将改变promise对象的状态:

下面则是一个 executor 以 error 拒绝 promise 的示例:
let promise = new Promise(function(resolve, reject) {
// 1 秒后发出工作已经被完成的信号,并带有 error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});对 reject(...) 的调用将 promise 对象的状态移至 "rejected":

总而言之,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolve 或 reject 来改变对应的 promise 对象的状态。
与最初的 “pending” promise 相反,一个 resolved 或 rejected 的 promise 都会被称为 “settled”。
只有一个结果或一个 error
executor 只能调用一个
resolve或一个reject。任何状态的更改都是最终的。所有其他的再对
resolve和reject的调用都会被忽略: javascriptlet promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // 被忽略 setTimeout(() => resolve("…")); // 被忽略 });关键就在于,一个由 executor 完成的工作只能有一个结果或一个 error。
并且,
resolve/reject只需要一个参数(或不包含任何参数),并且将忽略额外的参数。
以
Error对象 reject如果什么东西出了问题,executor 应该调用
reject。这可以使用任何类型的参数来完成(就像resolve一样)。但建议使用Error对象(或继承自Error的对象)。这样做的理由很快就会显而易见。
resolve/reject 可以立即进行
实际上,executor 通常是异步执行某些操作,并在一段时间后调用
resolve/reject,但这不是必须的。我们还可以立即调用resolve或reject,就像这样: javascriptlet promise = new Promise(function(resolve, reject) { // 不花时间去做这项工作 resolve(123); // 立即给出结果:123 });例如,当我们开始做一个任务,随后发现一切都已经完成并已被缓存时,可能就会发生这种情况。
state和result都是内部的Promise 对象的
state和result属性都是内部的。我们无法直接访问它们。但我们可以对它们使用.then/.catch/.finally方法。我们在下面对这些方法进行了描述。
消费者:then,catch
Promise 对象充当的是 executor(“生产者代码”)和消费函数之间的连接,后者将接收结果或 error。可以通过使用 .then 和 .catch 方法注册消费函数。
then
最重要最基础的一个就是 .then。
语法如下:
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。
.then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。
例如,以下是对成功 resolved 的 promise 做出的反应:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve 运行 .then 中的第一个函数
promise.then(
result => alert(result), // 1 秒后显示 "done!"
error => alert(error) // 不运行
);第一个函数被运行了。
在 reject 的情况下,运行第二个:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject 运行 .then 中的第二个函数
promise.then(
result => alert(result), // 不运行
error => alert(error) // 1 秒后显示 "Error: Whoops!"
);如果我们只对成功完成的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // 1 秒后显示 "done!"catch
如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!".catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式。
清理:finally
就像常规 try {...} catch {...} 中的 finally 子句一样,promise 中也有 finally。
调用 .finally(f) 类似于 .then(f, f),因为当 promise settled 时 f 就会执行:无论 promise 被 resolve 还是 reject。
finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。
例如,停止加载指示器,关闭不再需要的连接等。
把它想象成派对的终结者。无论派对是好是坏,有多少朋友参加,我们都需要(或者至少应该)在它之后进行清理。
代码可能看起来像这样:
new Promise((resolve, reject) => {
/* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
// 在 promise 为 settled 时运行,无论成功与否
.finally(() => stop loading indicator)
// 所以,加载指示器(loading indicator)始终会在我们继续之前停止
.then(result => show result, err => show error)请注意,finally(f) 并不完全是 then(f,f) 的别名。
它们之间有重要的区别:
-
finally处理程序(handler)没有参数。在finally中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。请看上面的例子:如你所见,
finally处理程序没有参数,promise 的结果由下一个处理程序处理。 -
finally处理程序将结果或 error “传递”给下一个合适的处理程序。例如,在这结果被从
finally传递给了then:javascriptnew Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000) }) .finally(() => alert("Promise ready")) // 先触发 .then(result => alert(result)); // <-- .then 显示 "value"正如我们所看到的,第一个 promise 返回的
value通过finally被传递给了下一个then。这非常方便,因为
finally并不意味着处理一个 promise 的结果。如前所述,无论结果是什么,它都是进行常规清理的地方。下面是一个 promise 返回结果为 error 的示例,让我们看看它是如何通过
finally被传递给catch的:javascriptnew Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // 先触发 .catch(err => alert(err)); // <-- .catch 显示这个 error -
finally处理程序也不应该返回任何内容。如果它返回了,返回的值会默认被忽略。此规则的唯一例外是当
finally处理程序抛出 error 时。此时这个 error(而不是任何之前的结果)会被转到下一个处理程序。
总结:
finally处理程序没有得到前一个处理程序的结果(它没有参数)。而这个结果被传递给了下一个合适的处理程序。- 如果
finally处理程序返回了一些内容,那么这些内容会被忽略。 - 当
finally抛出 error 时,执行将转到最近的 error 的处理程序。
如果我们正确使用 finally(将其用于常规清理),那么这些功能将很有用。
我们可以对 settled 的 promise 附加处理程序
如果 promise 为 pending 状态,
.then/catch/finally处理程序(handler)将等待它的结果。有时候,当我们向一个 promise 添加处理程序时,它可能已经 settled 了。
在这种情况下,这些处理程序会立即执行:
javascript// 下面这 promise 在被创建后立即变为 resolved 状态 let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done!(立刻显示)Promise 则更加灵活。我们可以随时添加处理程序(handler):如果结果已经在了,它们就会执行。
使用 promise 进行错误处理
promise 链在错误(error)处理中十分强大。当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。这在实际开发中非常方便。
例如,下面代码中所 fetch 的 URL 是错的(没有这个网站),.catch 对这个 error 进行了处理:
fetch('https://no-such-server.blabla') // reject
.then(response => response.json())
.catch(err => alert(err)) // TypeError: Failed to fetch(这里的文字可能有所不同)正如你所看到的,.catch 不必是立即的。它可能在一个或多个 .then 之后出现。
或者,可能该网站一切正常,但响应不是有效的 JSON。捕获所有 error 的最简单的方法是,将 .catch 附加到链的末尾:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));通常情况下,这样的 .catch 根本不会被触发。但是如果上述任意一个 promise rejected(网络问题或者无效的 json 或其他),.catch 就会捕获它。
隐式 try…catch
promise 的执行者(executor)和 promise 的处理程序周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。
例如,下面这段代码:
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!……与下面这段代码工作上完全相同:
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!在 executor 周围的“隐式 try..catch”自动捕获了 error,并将其变为 rejected promise。
这不仅仅发生在 executor 函数中,同样也发生在其处理程序中。如果我们在 .then 处理程序中 throw,这意味着 promise rejected,因此控制权移交至最近的 error 处理程序。
这是一个例子:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // reject 这个 promise
}).catch(alert); // Error: Whoops!对于所有的 error 都会发生这种情况,而不仅仅是由 throw 语句导致的这些 error。例如,一个编程错误:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // 没有这个函数
}).catch(alert); // ReferenceError: blabla is not defined最后的 .catch 不仅会捕获显式的 rejection,还会捕获它上面的处理程序中意外出现的 error。
再次抛出(Rethrowing)
正如我们已经注意到的,链尾端的 .catch 的表现有点像 try..catch。我们可能有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。
在常规的 try..catch 中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出。对于 promise 来说,这也是可以的。
如果我们在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序。如果我们处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。
在下面这个例子中,.catch 成功处理了 error:
// 执行流:catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) {
alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));这里 .catch 块正常完成。所以下一个成功的 .then 处理程序就会被调用。
在下面的例子中,我们可以看到 .catch 的另一种情况。(*) 行的处理程序捕获了 error,但无法处理它(例如,它只知道如何处理 URIError),所以它将其再次抛出:
// 执行流:catch -> catch
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 处理它
} else {
alert("Can't handle such error");
throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
}
}).then(function() {
/* 不在这里运行 */
}).catch(error => { // (**)
alert(`The unknown error has occurred: ${error}`);
// 不会返回任何内容 => 执行正常进行
});执行从第一个 .catch (*) 沿着链跳转至下一个 (**)。
未处理的 rejection
当一个 error 没有被处理会发生什么?例如,我们忘了在链的尾端附加 .catch,像这样:
new Promise(function() {
noSuchFunction(); // 这里出现 error(没有这个函数)
})
.then(() => {
// 一个或多个成功的 promise 处理程序
}); // 尾端没有 .catch!如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。但上面这个例子中并没有这样的处理程序。因此 error 会“卡住”。没有代码来处理它。
在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题。
当发生一个常规的 error 并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事。
JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error。如果你运行上面这个代码,你可以在控制台中看到。
在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:
window.addEventListener('unhandledrejection', function(event) {
// 这个事件对象有两个特殊的属性:
alert(event.promise); // [object Promise] —— 生成该全局 error 的 promise
alert(event.reason); // Error: Whoops! —— 未处理的 error 对象
});
new Promise(function() {
throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch这个事件是 HTML 标准 的一部分。
如果出现了一个 error,并且在这没有 .catch,那么 unhandledrejection 处理程序就会被触发,并获取具有 error 相关信息的 event 对象,所以我们就能做一些后续处理了。
Promise API
在 Promise 类中,有 6 种静态方法。我们在这里简单介绍下它们的使用场景。
Promise.all
假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。
例如,并行下载几个 URL,并等到所有内容都下载完毕后再对它们进行处理。
这就是 Promise.all 的用途。
语法:
let promise = Promise.all(iterable);Promise.all 接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。
当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。
例如,下面的 Promise.all 在 3 秒之后 settled,然后它的结果就是一个 [1, 2, 3] 数组:
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素请注意,结果数组中元素的顺序与其在源 promise 中的顺序相同。即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个。
一个常见的技巧是,将一个任务数据数组映射(map)到一个 promise 数组,然后将其包装到 Promise.all。
例如,如果我们有一个存储 URL 的数组,我们可以像这样 fetch 它们:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// 将每个 url 映射(map)到 fetch 的 promise 中
let requests = urls.map(url => fetch(url));
// Promise.all 等待所有任务都 resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));一个更真实的示例,通过 GitHub 用户名来获取一个 GitHub 用户数组中用户的信息(我们也可以通过商品 id 来获取商品数组中的商品信息,逻辑都是一样的):
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// 所有响应都被成功 resolved
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
}
return responses;
})
// 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
.then(responses => Promise.all(responses.map(r => r.json())))
// 所有 JSON 结果都被解析:"users" 是它们的数组
.then(users => users.forEach(user => alert(user.name)));如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。
例如:
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!这里的第二个 promise 在两秒后 reject。这立即导致了 Promise.all 的 reject,因此 .catch 执行了:被 reject 的 error 成为了整个 Promise.all 的结果。
如果出现 error,其他 promise 将被忽略
如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。
例如,像上面那个例子,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略。
Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念。在 另一个章节 中,我们将介绍可以帮助我们解决这个问题(译注:指的是“取消” promise)的 AbortController,但它不是 Promise API 的一部分。
Promise.all(iterable) 允许在 iterable 中使用非 promise 的“常规”值
通常,Promise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组。
例如,这里的结果是 [1, 2, 3]:
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3所以我们可以在方便的地方将准备好的值传递给 Promise.all。
Promise.allSettled
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render 方法需要所有 fetch 的数据Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的:
- 对成功的响应,结果数组对应元素的内容为
{status:"fulfilled", value:result}, - 对出现 error 的响应,结果数组对应元素的内容为
{status:"rejected", reason:error}。
例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣。
让我们使用 Promise.allSettled:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});上面的 (*) 行中的 results 将会是:
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]所以,对于每个 promise,我们都得到了其状态(status)和 value/reason。
Polyfill
如果浏览器不支持 Promise.allSettled,很容易进行 polyfill:
if (!Promise.allSettled) {
const rejectHandler = reason => ({ status: 'rejected', reason });
const resolveHandler = value => ({ status: 'fulfilled', value });
Promise.allSettled = function (promises) {
const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
return Promise.all(convertedPromises);
};
}在这段代码中,promises.map 获取输入值,并通过 p => Promise.resolve(p) 将输入值转换为 promise(以防传递了非 promise 值),然后向每一个 promise 都添加 .then 处理程序。
这个处理程序将成功的结果 value 转换为 {status:'fulfilled', value},将 error reason 转换为 {status:'rejected', reason}。这正是 Promise.allSettled 的格式。
然后我们就可以使用 Promise.allSettled 来获取 所有 给定的 promise 的结果,即使其中一些被 reject。
Promise.race
与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。
语法:
let promise = Promise.race(iterable);例如,这里的结果将是 1:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。
Promise.any
与 Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。如果给出的 promise 都 rejected,那么返回的 promise 会带有 AggregateError —— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。
语法如下:
let promise = Promise.any(iterable);例如,这里的结果将是 1:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1这里的第一个 promise 是最快的,但 rejected 了,所以第二个 promise 则成为了结果。在第一个 fulfilled 的 promise “赢得比赛”后,所有进一步的结果都将被忽略。
这是一个所有 promise 都失败的例子:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error!
});正如你所看到的,我们在 AggregateError 错误类型的 error 实例的 errors 属性中可以访问到失败的 promise 的 error 对象。
Promise.resolve/reject
在现代的代码中,很少需要使用 Promise.resolve 和 Promise.reject 方法,因为 async/await 语法使它们变得有些过时了。
完整起见,以及考虑到那些出于某些原因而无法使用 async/await 的人,我们在这里对它们进行介绍。
Promise.resolve
Promise.resolve(value) 用结果 value 创建一个 resolved 的 promise。
如同:
let promise = new Promise(resolve => resolve(value));当一个函数被期望返回一个 promise 时,这个方法用于兼容性。(译注:这里的兼容性是指,我们直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求。)
例如,下面的 loadCached 函数获取(fetch)一个 URL 并记住其内容。以便将来对使用相同 URL 的调用,它能立即从缓存中获取先前的内容,但使用 Promise.resolve 创建了一个该内容的 promise,所以返回的值始终是一个 promise。
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}我们可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。我们就可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的。
Promise.reject
Promise.reject(error) 用 error 创建一个 rejected 的 promise。
如同:
let promise = new Promise((resolve, reject) => reject(error));实际上,这个方法几乎从未被使用过。
async/await
async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
async function
让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:
async function f() {
return 1;
}在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
例如,下面这个函数返回一个结果为 1 的 resolved promise,让我们测试一下:
async function f() {
return 1;
}
f().then(alert); // 1……我们也可以显式地返回一个 promise,结果是一样的:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。
await
语法如下:
// 只在 async 函数内工作
let value = await promise;关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
这里的例子就是一个 1 秒后 resolve 的 promise:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
alert(result); // "done!"
}
f();这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。
让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写。
不能在普通函数中使用 await
如果我们尝试在非 async 函数中使用 await,则会报语法错误:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}如果我们忘记在函数前面写 async 关键字,我们可能会得到一个这个错误。就像前面说的,await 只在 async 函数中有效。
让我们拿 Promise 链 那一章的 showAvatar() 例子,并将其改写成 async/await 的形式:
- 我们需要用
await替换掉.then的调用。 - 另外,我们需要在函数前面加上
async关键字,以使它们能工作。
async function showAvatar() {
// 读取我们的 JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// 读取 github 用户信息
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// 显示头像
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// 等待 3 秒
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();简洁明了,是吧?比之前可强多了。
现代浏览器在 modules 里允许顶层的 await
在现代浏览器中,当我们处于一个 module 中时,那么在顶层使用 await 也是被允许的。我们将在 模块 (Module) 简介 中详细学习 modules。
例如:
// 我们假设此代码在 module 中的顶层运行
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);如果我们没有使用 modules,或者必须兼容 旧版本浏览器 ,那么这儿还有一个通用的方法:包装到匿名的异步函数中。
像这样:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();await 接受 “thenables”
像 promise.then 那样,await 允许我们使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await。
这有一个用于演示的 Thenable 类,下面的 await 接受了该类的实例:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// 1000ms 后使用 this.num*2 进行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// 等待 1 秒,之后 result 变为 2
let result = await new Thenable(1);
alert(result);
}
f();如果 await 接收了一个非 promise 的但是提供了 .then 方法的对象,它就会调用这个 .then 方法,并将内建的函数 resolve 和 reject 作为参数传入(就像它对待一个常规的 Promise executor 时一样)。然后 await 等待直到这两个函数中的某个被调用(在上面这个例子中发生在 (*) 行),然后使用得到的结果继续执行后续任务。
Class 中的 async 方法
要声明一个 class 中的 async 方法,只需在对应方法前面加上 async 即可:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1(alert 等同于 result => alert(result))这里的含义是一样的:它确保了方法的返回值是一个 promise 并且可以在方法中使用 await。
Error 处理
如果一个 promise 正常 resolve,await promise 返回的就是其结果。但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样。
这个代码:
async function f() {
await Promise.reject(new Error("Whoops!"));
}……和下面是一样的:
async function f() {
throw new Error("Whoops!");
}在真实开发中,promise 可能需要一点时间后才 reject。在这种情况下,在 await 抛出(throw)一个 error 之前会有一个延时。
我们可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();如果有 error 发生,执行控制权马上就会被移交至 catch 块。我们也可以用 try 包装多行 await 代码:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// 捕获到 fetch 和 response.json 中的错误
alert(err);
}
}
f();如果我们没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将变为 rejected。我们可以在函数调用后面添加 .catch 来处理这个 error:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)如果我们忘了在这添加 .catch,那么我们就会得到一个未处理的 promise error(可以在控制台中查看)。
async/await 和 promise.then/catch
当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。这通常(但不总是)更加方便。
但是当我们在代码的顶层时,也就是在所有 async 函数之外,我们在语法上就不能使用 await 了,所以这时候通常的做法是添加 .then/catch 来处理最终的结果(result)或掉出来的(falling-through)error,例如像上面那个例子中的 (*) 行那样。
async/await 可以和 Promise.all 一起使用
当我们需要同时等待多个 promise 时,我们可以用 Promise.all 把它们包装起来,然后使用 await:
// 等待结果数组
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);如果出现 error,也会正常传递,从失败了的 promise 传到 Promise.all,然后变成我们能通过使用 try..catch 在调用周围捕获到的异常(exception)。
评论
评论加载中……