- 原文地址:Deeply Understanding JavaScript Async and Await with Examples
- 原文做者:Arfat Salman
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:xionglong58
- 校对者:Baddyo,Mcskiller,fireairforce
首先来了解下回调函数。回调函数会在被调用后的某一时刻执行,除此以外与其余普通函数并没有差异。因为 JavaScript 的异步特征,在一些不能当即得到函数返回值的地方都须要使用回调函数。javascript
下面是一个 Node.js 读取文件时的示例(异步操做)——前端
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
复制代码
但当咱们要处理多重异步操做时问题就会凸显出来。假设有下面的应用场景(其中的全部操做都是异步的)——java
Arfat
,读取 profile_img_url
数据,而后把图片从 someServer.com
上下载下来。Arfat
发送 email。transformations.log
并加上时间戳。上述过程的代码大体以下 ——android
注意回调函数的嵌套和程序末尾 })
的层级。 鉴于结构上的类似性,这种方式被形象地称做回调地狱或回调金字塔。这种方式的一些缺点是 ——ios
为了解决上述问题,JavaScript 提出了 Promise。如今,咱们可使用链式结构取代回调函数嵌套的结构。下面是一个例子 ——git
回调流程由从左至右结构变成咱们所熟悉的自上而下的结构,这是一个优势。可是 promise 仍然有一些缺点 ——es6
.then
中处理回调。try/catch
,咱们须要使用 .catch
处理错误。为了证实上面的最后一个缺点,尝试一下下面的挑战吧!github
假设要在 for 循环中以任意时间间隔(0 到 n 秒)输出数字 0 到 10。咱们将使用 promise 去顺序打印 0 到 10,好比打印 0 须要 6 秒,打印 1 要延迟 2 秒,而 1 须要 0 打印完成以后才能打印,其它数字打印过程也相似。数据库
固然,不要使用 async/await
或 .sort
方法,随后咱们将会解决这一问题。json
async 函数在 ES2017 (ES8) 中引入,使得 promise 的应用更加简单。
所以,在理解 async/await 概念以前你必需要对 promise 有所了解。
async/await 包含两个关键字 async 和 await。async
用来使得函数能够异步执行。async
使得在函数中可使用 await
关键字,除此以外,在任何地方使用 await
都属于语法错误。
// 应用到普通的声明函数
async function myFn() {
// await ...
}
// 应用到箭头函数
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
复制代码
注意,在函数声明中 async
关键字位于声明的前面。在箭头函数中,async
关键字则位于 =
和圆括号的中间。
async 函数还能做为对象的方法,或是像下面代码同样位于类中。
// 做为对象方法
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// 位于类中
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}
复制代码
注意:类的构造函数和 getters/setters 不能做为 async 函数。
async 函数与普通 JavaScript 函数相比有如下区别 ——
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
复制代码
函数 fn
的返回值 'hello'
,因为咱们使用了 async
关键字,返回值 'hello' 被包装成了一个 promise 对象(经过 Promise.resolve
实现)。
所以,不使用 async
关键字的具备同等做用的替代方案可写做 ——
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
复制代码
在上面的代码中咱们手动返回了一个 promise 对象用于替换 async
关键字。
确切地说,async 函数的返回值将会被传递到 Promise.resolve
方法中。
若是返回值是一个原始值,Promise.resolve
则返回该值的一个 promise 版本。可是,若是返回值是 promise 对象,那么 Promise.resolve
将原封不动地返回这个对象。
// 返回值是原始值的状况
const p = Promise.resolve('hello')
p instanceof Promise;
// true
//p 被原封不动地返回
Promise.resolve(p) === p;
// true
复制代码
在 async 函数中抛出一个错误会发生什么?
好比 ——
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
复制代码
若是错误未被捕获,foo()
函数会返回一个状态为 rejected 的 promise。不一样于 Promise.resolve
,Promise.reject
会包装错误并返回。详情请看稍后的错误处理部分。
最终结果是,不论你想要返回什么结果,最终在 async 函数外,你都会获得一个 promise。
await
命令就像一个表达式同样。当 await 后面跟着一个 promise 时,async 函数遇到 await 会停止运行,直到相应的 promise 状态变成 resolved。当 await 后面跟的是原始值时,原始值会被传入 Promise.resolve
而转变成一个 promise 对象,而且状态为 resolved。
// 多功能函数:获取随机值/延时
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// 执行函数 fn
fn().then(console.log);
复制代码
让咱们来逐行检验函数 fn
——
当函数 fn
被调用时,首先被执行的是 const a = await 9;
。它被隐式地转换成 const a = await Promise.resolve(9);
。
因为咱们使用了 await
命令,fn
函数会在此时会暂停到变量 a 得到值为止。在该状况下 Promise.resolve
方法返回值为 9。
delayAndGetRandom(1000)
函数使得 fn
中的其它程序暂停执行,直到 1 秒钟以后 delayAndGetRandom
状态转变成 resolved。因此,fn
函数的执行有效地暂停了 1 秒钟。
此外,delayAndGetRandom
中的 resolve 函数返回一个随机值。不管往 resolve
函数中传入什么值, 都会赋值给变量 b
。
一样,变量 c
值为 5
,而后使用 await delayAndGetRandom(1000)
又延时了 1 秒钟。在这个例子中咱们并无使用 Promise.resolve
返回值。
最后咱们计算 a + b * c
的结果,经过 Promise.resolve
将该结果包装成一个 promise,并将其做为 async 函数的返回值。
注意: 若是上面程序的暂停和恢复操做让你想起了 ES6 的 generator,那是由于 generator 也有不少优势。
让咱们使用 async/await 解决在前面提出的假设问题 ——
咱们定义了一个 async 函数 finishMyTask
,使用 await
去等待 queryDatabase
、sendEmail
、logTaskInFile
的操做结果。
若是咱们将 async/await 解决方案与使用 promise 的方案进行对比以后会发现代码的数量很相近。可是 async/await 使得代码在语法复杂性方面变得更简单,不用去记忆多层回调函数以及 .then
/.catch
。
如今,就让咱们解决上面所列的打印数字的挑战。下面是两种不一样的解决方法 ——
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// 方法一(使用 for 循环)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// 方法二(使用回调)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};
复制代码
你能够在 repl.it console 上运行上面的代码。
若是容许你使用 async 函数,那么这个挑战解决起来将会简单得多。
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
复制代码
一样,该方法也能够在 repl.it console 上运行。
如同咱们在语法部分所见,一个未捕获的 Error()
被包装在一个 rejected promise 中。可是,咱们能够在 async 函数中同步地使用 try-catch
处理错误。让咱们从这一实用的函数开始 ——
async function canRejectOrReturn() {
// 等待一秒
await new Promise(res => setTimeout(res, 1000));
// 50% 的可能性是 Rejected 状态
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
复制代码
canRejectOrReturn()
是一个 async 函数,他可能返回 'perfect number'
也可能抛出错误('Sorry, number too big')。
咱们来看看示例代码 ——
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
复制代码
由于咱们在等待执行 canRejectOrReturn
函数的时候,canRejectOrReturn 函数体内的 promise 会转移到 rejected 状态而抛出错误,这将致使 catch
代码块被执行。也就是说 foo
函数运行结果为 rejected
,返回值为 undefined
(由于咱们在 try
中没有返回值)或者 'error caught'
。由于咱们在 foo
函数中使用了 try-catch
处理错误,因此说 foo
函数的结果永远不会是 rejected。
下面是另一个版本的例子 ——
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
复制代码
注意这一次咱们使用了 return (而不是 await)将函数 canRejectOrReturn
从 foo
函数中返回。foo
函数运行结果是 resolved,返回值为 'perfect number'
或者值为 Error('Sorry, number too big')。catch
代码块永远都不会被执行。
这是由于函数 foo
返回了 canRejectOrReturn
返回的 promise 对象。所以 foo
的 resolved 变成了 canRejectOrReturn
的 resolved。你能够将 return canRejectOrReturn()
等价为下面两行程序去理解(注意第一行没有 await)——
try {
const promise = canRejectOrReturn();
}
复制代码
让咱们看看 await
和 return
搭配使用时的状况 ——
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
复制代码
在上面的例子中,foo
函数运行结果为 resolved,返回值为 'perfect number'
或 'error caught'
。foo
函数的结果永远不会是 rejected。 这就像上面那个只有 await
的例子。只是这里将函数 canRejectOrReturn
的 rejected 结果返回了,而不是返回了 undefined
。
你能够将语句 return await canRejectOrReturn();
拆开再看看效果 ——
try {
const value = await canRejectOrReturn();
return value;
}
// ...
复制代码
因为涉及 promise 和 async/await 之间错综复杂的操做,程序中可能会潜藏一些细微的差错。让咱们一块儿看看吧 ——
有时候,在 promise 对象以前咱们忘记了使用 await
关键字,或者是忘记将 promise 对象返回。以下所示 ——
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}
复制代码
注意咱们并无使用 await
或 return
。foo
函数运行结果为返回值是 undefined
的 resolved,而且函数执行不会延迟 1 秒钟。可是canRejectOrReturn() 中的 promise 的确被执行了。若是没有反作用产生,这的确会发生。若是 canRejectOrReturn() 抛出错误或者状态转移为 rejected,UnhandledPromiseRejectionWarning 错误将会产生。
咱们常常把 async 函数做为.map
或 .filter
方法的回调。让咱们举个例子 — 假设咱们有一个函数 fetchPublicReposCount(username) 能够获取一个 github 用户拥有的公开仓库的数量。咱们想要得到三名不一样用户的公开仓库数量,让咱们来看代码 —
const url = 'https://api.github.com/users';
// 使用 fn 函数获取仓库数量
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}
复制代码
想要得到三名用户 ['ArfatSalman', 'octocat', 'norvig'] 的公开仓库数量。咱们可能会这样作 ——
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
复制代码
注意 async
在 .map
方法中。咱们可能但愿变量 counts
存储着的公开仓库数量。可是,就如咱们以前所见,全部的 async 函数均返回 promise 对象。 所以,counts
其实是一个 promise 对象数组。.map
为每个 username
调用异步函数,.map
方法将每次调用返回的 promise 结果保存在数组中。
咱们可能也会有其它解决方法,好比 ——
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
复制代码
咱们手动获取了每个 count,并将它们 append 到 counts
数组中。程序的问题在于第一个用户的 count 被获取以后,第二个用户的 count 才能被获取。同一时间,只有一个公开仓库数量能够被获取。
若是一个 fetch 操做耗时 300 ms,那么 fetchAllCounts
函数耗时大概在 900 ms 左右。因而可知,程序耗时会随着用户数量的增长而线性增长。由于获取不一样用户公开仓库数量之间没有依赖,咱们能够将操做并行处理。
咱们能够同时获取用户的公开仓库数量,而不是顺序获取。咱们将使用 .map
方法和 Promise.all
。
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}
复制代码
Promise.all
接受一个 promise 对象数组做为输入,返回一个 promise 对象。当全部 promise 对象的状态都转变成 resolved 时,返回值为全部 promise 对应返回值组成的 promise 数组,只要有一个 promise 对象被 rejected,Promise.all
的返回值为第一个被 rejected 的 promise 对象对应的返回值。可是,同时运行全部 promise 的操做可能行不通。可能你想批量执行 promise。你能够考虑下使用 p-map 实现受限的并发。
async 函数变得很重要。随着 Async Iterators 的引入,async 函数将会应用得愈来愈广。对于现代 JavaScript 开发人员来讲深刻理解 async 函数相当重要。我但愿这篇文章能对你有所启发。:)
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。