使用 promise 进行错误处理
概述
promise链在错误(error)处理中十分强大- 当一个
promise被reject时,控制权将移交至最近的rejection处理程序 - 例如,下面代码中所
fetch的URL是错的(没有这个网站),.catch对这个error进行了处理:- 如你所看到的,
.catch不必是立即的。它可能在一个或多个.then之后出现 - 或者,可能该网站一切正常,但响应不是有效的
JSON
- 如你所看到的,
|
1 2 3 |
fetch('https://no-such-server.blabla') // reject .then(response => response.json()) .catch(err => alert(err)) // TypeError: Failed to fetch(这里的文字可能有所不同) |
- 捕获所有
error的最简单的方法是,将.catch附加到链的末尾:- 通常情况下,这样的
.catch根本不会被触发 - 但是如果上述任意一个
promise rejected(网络问题或者无效的json或其他),.catch就会捕获它
- 通常情况下,这样的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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)); |
隐式 try…catch
promise的执行者(executor)和promise的处理程序周围有一个“隐式的try..catch”- 如果发生异常,它就会被捕获,并被视为
rejection进行处理 - 例如,下面这段代码:
|
1 2 3 |
new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(alert); // Error: Whoops! |
- 与下面这段代码工作上完全相同:
|
1 2 3 |
new Promise((resolve, reject) => { reject(new Error("Whoops!")); }).catch(alert); // Error: Whoops! |
- 在
executor周围的“隐式try..catch”自动捕获了error,并将其变为rejected promise- 这不仅仅发生在
executor函数中,同样也发生在其处理程序中 - 如果我们在
.then处理程序中throw,这意味着promise rejected,因此控制权移交至最近的error处理程序 - 对于所有的
error都会发生这种情况,而不仅仅是由throw语句导致的这些error
- 这不仅仅发生在
|
1 2 3 4 5 |
new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { throw new Error("Whoops!"); // reject 这个 promise }).catch(alert); // Error: Whoops! |
- 例如,一个编程错误:
- 最后的
.catch不仅会捕获显式的rejection,还会捕获它上面的处理程序中意外出现的error
- 最后的
|
1 2 3 4 5 |
new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { blabla(); // 没有这个函数 }).catch(alert); // ReferenceError: blabla is not defined |
再次抛出(Rethrowing)
- 如我们已经注意到的,链尾端的
.catch的表现有点像try..catch - 们可能有许多个
.then处理程序,然后在尾端使用一个.catch处理上面的所有error- 在常规的
try..catch中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出 - 对于
promise来说,这也是可以的
- 在常规的
- 如果我们在
.catch中throw,那么控制权就会被移交到下一个最近的error处理程序- 如果我们处理该
error并正常完成,那么它将继续到最近的成功的.then处理程序 - 在下面这个例子中,
.catch成功处理了error: - 这里
.catch块正常完成。所以下一个成功的.then处理程序就会被调用
- 如果我们处理该
|
1 2 3 4 5 6 7 8 9 10 |
// 执行流: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的另一种情况(*)行的处理程序捕获了error,但无法处理它(例如,它只知道如何处理URIError),所以它将其再次抛出:- 执行从第一个
.catch(*)沿着链跳转至下一个(**)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 执行流: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}`); // 不会返回任何内容 => 执行正常进行 }); |
未处理的 rejection
- 当一个
error没有被处理会发生什么?例如,我们忘了在链的尾端附加.catch,像这样:
|
1 2 3 4 5 6 |
new Promise(function() { noSuchFunction(); // 这里出现 error(没有这个函数) }) .then(() => { // 一个或多个成功的 promise 处理程序 }); // 尾端没有 .catch! |
- 如果出现
error,promise的状态将变为 “rejected”,然后执行应该跳转至最近的rejection处理程序。但上面这个例子中并没有这样的处理程序- 因此
error会“卡住”。没有代码来处理它
- 因此
- 当发生一个常规的
error并且未被try..catch捕获时会发生什么?- 脚本死了,并在控制台中留下了一个信息
- 对于在
promise中未被处理的rejection,也会发生类似的事
JavaScript引擎会跟踪此类rejection,在这种情况下会生成一个全局的error- 在浏览器中,我们可以使用
unhandledrejection事件来捕获这类error:
- 在浏览器中,我们可以使用
|
1 2 3 4 5 6 7 8 9 |
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接受一个可迭代对象(通常是一个数组项为promise的数组),并返回一个新的promise- 当所有给定的
promise都resolve时,新的promise才会resolve,并且其结果数组将成为新promise的结果
|
1 |
let promise = Promise.all(iterable); |
|
1 2 3 4 5 |
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它们:
- 即使第一个
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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用户数组中用户的信息
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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在两秒后reject。这立即导致了Promise.all的reject - 因此
.catch执行了:被reject的error成为了整个Promise.all的结果
- 这里的第二个
|
1 2 3 4 5 |
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.all(iterable)允许在iterable中使用非promise的“常规”值- 通常,
Promise.all(...)接受含有promise项的可迭代对象(大多数情况下是数组)作为参数 - 但是,如果这些对象中的任何一个不是
promise,那么它将被“按原样”传递给结果数组 - 例如,这里的结果是
[1, 2, 3]: - 所以我们可以在方便的地方将准备好的值传递给
Promise.all
- 通常,
|
1 2 3 4 5 6 7 |
Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, 3 ]).then(alert); // 1, 2, 3 |
注意
- 如果出现
error,其他promise将被忽略- 如果其中一个
promise被reject,Promise.all就会立即被reject,完全忽略列表中其他的promise。它们的结果也被忽略
- 如果其中一个
Promise.allSettled
- 这是一个最近添加到
JavaScript的特性 - 如果任意的
promise reject,则Promise.all整个将会reject- 当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:
|
1 2 3 4 5 |
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)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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将会是:
|
1 2 3 4 5 |
[ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ] |
- 如果浏览器不支持
Promise.allSettled,很容易进行polyfill:
|
1 2 3 4 5 6 7 8 9 10 |
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); }; } |
Promise.race
- 与
Promise.all类似,但只等待第一个settled的promise并获取其结果(或error) - 语法:
|
1 |
let promise = Promise.race(iterable); |
- 例如,这里的结果将是
1:- 这里第一个
promise最快,所以它变成了结果 - 第一个
settled的promise“赢得了比赛”之后,所有进一步的result/error都会被忽略
- 这里第一个
|
1 2 3 4 5 |
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.any
- 与
Promise.race类似,区别在于Promise.any只等待第一个fulfilled的promise,并将这个fulfilled的promise返回 - 如果给出的
promise都rejected,那么返回的promise会带有AggregateError—— 一个特殊的error对象,在其errors属性中存储着所有promise error - 语法
|
1 |
let promise = Promise.any(iterable); |
- 例如,这里的结果将是
1:- 这里的第一个
promise是最快的,但rejected了 - 所以第二个
promise则成为了结果 - 在第一个
fulfilled的promise“赢得比赛”后,所有进一步的结果都将被忽略
- 这里的第一个
|
1 2 3 4 5 |
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都失败的例子:- 我们在
AggregateError错误类型的error实例的errors属性中可以访问到失败的promise的error对象
- 我们在
|
1 2 3 4 5 6 7 8 |
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! }); |
Promise.resolve/reject
- 现代的代码中,很少需要使用
Promise.resolve和Promise.reject方法,因为async/await语法 Promise.resolve(value)用结果value创建一个resolved的promise- 当一个函数被期望返回一个
promise时,这个方法用于兼容性 - 这里的兼容性是指,我们直接从缓存中获取了当前操作的结果
value,但是期望返回的是一个promise,所以可以使用Promise.resolve(value)将value“封装”进promise,以满足期望返回一个promise的这个需求
- 当一个函数被期望返回一个
|
1 |
let promise = new Promise(resolve => resolve(value)); |
- 例如,下面的
loadCached函数获取(fetch)一个URL并记住其内容。以便将来对使用相同URL的调用- 它能立即从缓存中获取先前的内容,但使用
Promise.resolve创建了一个该内容的promise,所以返回的值始终是一个promise
- 它能立即从缓存中获取先前的内容,但使用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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; }); } |
Promise.reject(error)用error创建一个rejected的promise
|
1 |
let promise = new Promise((resolve, reject) => reject(error)); |
Promisification
概述
- 它指将一个接受回调的函数转换为一个返回
promise的函数 - 由于许多函数和库都是基于回调的,因此,在实际开发中经常会需要进行这种转换
- 因为使用
promise更加方便,所以将基于回调的函数和库promise化是有意义的
- 因为使用
- 例如,在 回调 一章中我们有
loadScript(src, callback)- 该函数通过给定的
src加载脚本,然后在出现错误时调用callback(err),或者在加载成功时调用callback(null, script)
- 该函数通过给定的
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } // 用法: // loadScript('path/script.js', (err, script) => {...}) |
- 现在,让我们将其
promise化吧- 我们将创建一个新的函数
loadScriptPromise(src),与上面的函数作用相同(加载脚本),只是我们创建的这个函数会返回一个 promise 而不是使用回调 - 换句话说,我们仅向它传入
src(没有callback)并通过该函数的return获得一个promise,当脚本加载成功时,该promise将以script为结果resolve,否则将以出现的error为结果reject - 正如我们所看到的,新的函数是对原始的
loadScript函数的包装
新函数调用它,并提供了自己的回调来将其转换成promiseresolve/reject
- 我们将创建一个新的函数
|
1 2 3 4 5 6 7 8 9 10 11 |
let loadScriptPromise = function(src) { return new Promise((resolve, reject) => { loadScript(src, (err, script) => { if (err) reject(err); else resolve(script); }); }); }; // 用法: // loadScriptPromise('path/script.js').then(...) |
- 在实际开发中,我们可能需要
promise化很多函数,所以使用一个helper(辅助函数)很有意义- 我们将其称为
promisify(f):它接受一个需要被promise化的函数f,并返回一个包装(wrapper)函数。
- 我们将其称为
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function promisify(f) { return function (...args) { // 返回一个包装函数(wrapper-function) (*) return new Promise((resolve, reject) => { function callback(err, result) { // 我们对 f 的自定义的回调 (**) if (err) { reject(err); } else { resolve(result); } } args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾 f.call(this, ...args); // 调用原始的函数 }); }; } // 用法: let loadScriptPromise = promisify(loadScript); loadScriptPromise(...).then(...); |
- 我们可以继续改进我们的辅助函数。让我们写一个更高阶版本的
promisify- 当它被以
promisify(f)的形式调用时,它应该以与上面那个版本的实现的工作方式类似 - 当它被以
promisify(f, true)的形式调用时,它应该返回以回调函数数组为结果resolve的promise。这就是具有很多个参数的回调的结果
- 当它被以
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// promisify(f, true) 来获取结果数组 function promisify(f, manyArgs = false) { return function (...args) { return new Promise((resolve, reject) => { function callback(err, ...results) { // 我们自定义的 f 的回调 if (err) { reject(err); } else { // 如果 manyArgs 被指定,则使用所有回调的结果 resolve resolve(manyArgs ? results : results[0]); } } args.push(callback); f.call(this, ...args); }); }; } // 用法: f = promisify(f, true); f(...).then(arrayOfResults => ..., err => ...); |
注意
Promisification是一种很好的方法,特别是在你使用async/await的时候,但不是回调的完全替代- 记住,一个
promise可能只有一个结果,但从技术上讲,一个回调可能被调用很多次 - 因此,
promisification仅适用于调用一次回调的函数。进一步的调用将被忽略
- 记住,一个
微任务(Microtask)
概述
promise的处理程序.then、.catch和.finally都是异步的- 即便一个
promise立即被 resolve,.then、.catch和.finally下面 的代码也会在这些处理程序之前被执行
|
1 2 3 4 5 |
let promise = Promise.resolve(); promise.then(() => alert("promise done!")); alert("code finished"); // 这个 alert 先显示 |
- 如果你运行它,你会首先看到
code finished,然后才是promise done- 这很奇怪,因为这个
promise肯定是一开始就完成的 - 为什么
.then会在之后才被触发?这是怎么回事?
- 这很奇怪,因为这个
微任务队列(Microtask queue)
- 异步任务需要适当的管理
- 为此,
ECMA标准规定了一个内部队列PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8术语)
- 为此,
- 如 规范中所述
- 队列(
queue)是先进先出的:首先进入队列的任务会首先运行 - 只有在
JavaScript引擎中没有其它任务在运行时,才开始执行任务队列中的任务
- 队列(
- 或者,简单地说,当一个
promise准备就绪时,它的.then/catch/finally处理程序就会被放入队列中:但是它们不会立即被执行- 当
JavaScript引擎执行完当前的代码,它会从队列中获取任务并执行它 promise的处理程序总是会经过这个内部队列
- 当
- 如果有一个包含多个
.then/catch/finally的链,那么它们中的每一个都是异步执行的- 也就是说,它会首先进入队列,然后在当前代码执行完成并且先前排队的处理程序都完成时才会被执行
- 如果执行顺序对我们很重要该怎么办?我们怎么才能让
code finished在promise done之后出现呢?- 很简单,只需要像下面这样使用
.then将其放入队列: - 现在代码就是按照预期执行的
- 很简单,只需要像下面这样使用
|
1 2 3 |
Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished")); |
未处理的 rejection
- 如果一个
promise的error未被在微任务队列的末尾进行处理,则会出现“未处理的rejection” - 正常来说,如果我们预期可能会发生错误,我们会在
promise链上添加.catch来处理error:
|
1 2 3 4 5 |
let promise = Promise.reject(new Error("Promise Failed!")); promise.catch(err => alert('caught')); // 不会运行:error 已经被处理 window.addEventListener('unhandledrejection', event => alert(event.reason)); |
- 但是如果我们忘记添加
.catch,那么,微任务队列清空后,JavaScript引擎会触发下面这事件:
|
1 2 3 4 5 |
let promise = Promise.reject(new Error("Promise Failed!")); // Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); |
- 如果我们迟一点再处理这个
error会怎样?例如:- 如果我们运行上面这段代码,我们会先看到
Promise Failed!,然后才是caught - 当微任务队列中的任务都完成时,才会生成
unhandledrejection:引擎会检查promise,如果promise中的任意一个出现 “rejected” 状态,unhandledrejection事件就会被触发 - 面这个例子中,被添加到
setTimeout中的.catch也会被触发。只是会在unhandledrejection事件出现之后才会被触发,所以它并没有改变什么(没有发挥作用)
- 如果我们运行上面这段代码,我们会先看到
|
1 2 3 4 5 |
let promise = Promise.reject(new Error("Promise Failed!")); setTimeout(() => promise.catch(err => alert('caught')), 1000); // Error: Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason)); |
async/await
概述
async/await是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用
async
- 让我们以
async这个关键字开始。它可以被放置在一个函数前面,如下所示:- 在函数前面的 “
async” 这个单词表达了一个简单的事情:即这个函数总是返回一个promise - 其他值将自动被包装在一个
resolved的promise中
- 在函数前面的 “
|
1 2 3 |
async function f() { return 1; } |
- 例如,下面这个函数返回一个结果为
1的resolved promise,让我们测试一下:
|
1 2 3 4 5 |
async function f() { return 1; } f().then(alert); // 1 |
- 我们也可以显式地返回一个
promise,结果是一样的:- 所以说,
async确保了函数返回一个promise,也会将非promise的值包装进去
- 所以说,
|
1 2 3 4 5 |
async function f() { return Promise.resolve(1); } f().then(alert); // 1 |
await
- 语法
- 关键字
await让JavaScript引擎等待直到promise完成(settle)并返回结果
- 关键字
|
1 2 |
// 只在 async 函数内工作 let value = await promise; |
- 这里的例子就是一个
1秒后resolve的promise:- 这个函数在执行的时候,“暂停”在了
(*)那一行,并在promise settle时,拿到result作为结果继续往下执行 await实际上会暂停函数的执行,直到promise状态变为settled,然后以promise的结果继续执行- 这个行为不会耗费任何
CPU资源,因为JavaScript引擎可以同时处理其他任务:执行其他脚本,处理事件等 - 相比于
promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写
- 这个函数在执行的时候,“暂停”在了
|
1 2 3 4 5 6 7 8 9 10 11 12 |
async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) }); let result = await promise; // 等待,直到 promise resolve (*) alert(result); // "done!" } f(); |
showAvatar()例子,并将其改写成async/await的形式:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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(); |
注意1
- 不能在普通函数中使用
await- 如果我们尝试在非
async函数中使用await,则会报语法错误:
- 如果我们尝试在非
|
1 2 3 4 |
function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error } |
- 现代浏览器在
modules里允许顶层的await- 在现代浏览器中,当我们处于一个
module中时,那么在顶层使用await也是被允许的
- 在现代浏览器中,当我们处于一个
|
1 2 3 4 5 |
// 我们假设此代码在 module 中的顶层运行 let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); console.log(user); |
await接受 “thenables”- 像
promise.then那样,await允许我们使用thenable对象(那些具有可调用的then方法的对象) - 这里的想法是,第三方对象可能不是一个
promise,但却是promise兼容的:如果这些对象支持.then,那么就可以对它们使用await。
- 像
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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(); |
Class中的async方法- 要声明一个
class中的async方法,只需在对应方法前面加上async即可: - 这里的含义是一样的:它确保了方法的返回值是一个
promise并且可以在方法中使用await
- 要声明一个
|
1 2 3 4 5 6 7 8 9 |
class Waiter { async wait() { return await Promise.resolve(1); } } new Waiter() .wait() .then(alert); // 1(alert 等同于 result => alert(result)) |
Error处理
- 如果一个
promise正常resolve,await promise返回的就是其结果- 但是如果
promise被reject,它将throw这个error,就像在这一行有一个throw语句那样
- 但是如果
|
1 2 3 |
async function f() { await Promise.reject(new Error("Whoops!")); } |
|
1 2 3 |
async function f() { throw new Error("Whoops!"); } |
- 在真实开发中,
promise可能需要一点时间后才reject。在这种情况下,在await抛出(throw)一个error之前会有一个延时- 我们可以用
try..catch来捕获上面提到的那个error,与常规的throw使用的是一样的方式:
- 我们可以用
|
1 2 3 4 5 6 7 8 9 10 |
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代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
|
1 2 3 4 5 6 |
async function f() { let response = await fetch('http://no-such-url'); } // f() 变成了一个 rejected 的 promise f().catch(alert); // TypeError: failed to fetch // (*) |
注意2
sync/await和promise.then/catch- 当我们使用
async/await时,几乎就不会用到.then了,因为await为我们处理了等待 - 并且我们使用常规的
try..catch而不是.catch
- 当我们使用
async/await可以和Promise.all一起使用- 当我们需要同时等待多个
promise时,我们可以用Promise.all把它们包装起来,然后使用await:
- 当我们需要同时等待多个
|
1 2 3 4 5 6 |
// 等待结果数组 let results = await Promise.all([ fetch(url1), fetch(url2), ... ]); |
- 如果出现
error,也会正常传递,从失败了的promise传到Promise.all,然后变成我们能通过使用try..catch在调用周围捕获到的异常(exception)
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!