使用 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
函数的包装
新函数调用它,并提供了自己的回调来将其转换成promise
resolve/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所有,欢迎分享本文,转载请保留出处!