原文:blog.bitsrc.io/understandi…
作者:Arfat Salman
翻译:前端小白

首先我们来讨论下回调函数,回调函数没什么特别的,只是在将来的某个时候执行的函数。由于JavScript的异步特性,在许多不能立即获得结果的地方都需要回调
这是一个Node.js异步读取文件的例子:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
当我们想要执行多个异步操作时,就会出现问题。想象以下的场景(所有操作都是异步的):
- 我们在数据库中查询用户
Arfat。 我们读取profile_img_url并从someServer.com获取图像。 - 获取图像后,我们将其转换为另一种格式,比如PNG到JPEG。
- 如果转换成功,我们将向用户发送电子邮件。
- 在
transformations.log中记录这个任务,并带上时间戳
代码大致如下:

注意回调函数的嵌套的末尾 }) 的层级关系,这种方式被戏称作 回调地狱 或 回调金字塔。缺点是 ——
- 代码可读性太差
- 错误处理很复杂,常常导致错误代码。
为了解决上述问题,JavaScript 提出了 Promise。现在,我们可以使用链式结构而不是回调函数嵌套的结构。下面是一个例子 ——

现在整个流程是自上而下而不是从左至右结构,这是一个优点。但是 promise 仍然有一些缺点 ——
- 在每个
.then中我们还是要处理回调 - 相比使用正常的
try/catch,我们要使用.catch()处理错误 - 在循环中按顺序处理多个
promises会非常头疼,不直观
我们来演示下关于最后一个缺点:
挑战
假设我们有一个for循环,它以随机间隔(0到n秒)打印0到10。我们需要使用 promise 按0到10顺序打印出来。例如,如果0打印需要6秒,1打印需要2秒,那么1应该等待0打印完之后再打印,以此类推。
不要使用 async/await 或者 .sort,之后我们会解决这个问题
Async 函数
ES2017(ES8) 中引入了async函数,使promise应用起来非常简单
- 很重要的一点需要注意:
async函数的使用是基于promise - 他们不是完全不同的概念
- 可以认为是一种基于
promise异步代码的替代方案 async/await可以避免使用promise链式调用- 代码异步执行,看起来是同步式的
因此,理解 async/await 必须要先了解 promise
语法
async/await 包含两个关键字 async 和 await。async 用来使得函数可以异步执行。async 可以让我们在函数中使用 await ,除此之外,在任何地方使用 await 都属于语法错误。
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
注意,在函数声明中 async 关键字在函数声明的前面。在箭头函数中,async 关键字则位于 = 和圆括号的中间。
async 函数还能作为对象的方法,或是在类的声明中。
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}
注意:类的构造函数和 getters/setters 不能使用 async 函数。
语义和评估准则
async是普通的JavaScript函数,它们有以下不同之处--
async 函数总是返回 promise 对象。
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
函数 fn 返回 'hello',由于我们使用了 async 关键字, 'hello' 被包装成了一个 promise 对象(通过 Promise 构造函数实现)
这是另外一种实现方式,不使用 async
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
上面代码中,我们手动返回了一个 promise 对象,没有使用 async 关键字
更准确地说,async函数的返回值会被 Promise.resolve 包裹。
如果返回值是原始值,Promise.resolve 会返回一个promise化的值,如果返回值是一个promise对象,则直接返回这个对象
// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise;
// true
// p is returned as is it
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。
async 函数遇到 await <表达式>时会暂停
await作用于一个表达式。当表达式是一个promise时,async函数会暂停执行,直到该promise状态变为 resolved。当表达式为非promise值时,会使用 Promise.resolve 将其转换为promise,然后状态变为 resolved。
// utility function to cause delay
// and get random value
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;
}
// Execute fn
fn().then(console.log);
让我们来逐行看看 fn 函数
- 当函数执行时,第一行代码
const a = await 9,内部会被解析为const a = await Promise.resolve(9) - 因为使用到了
await, 所以函数执行会暂停,直到变量a得到一个值,在promise 会将其resolve为9 delayAndGetRandom(1000)会使fn函数暂停,直到1秒钟之后delayAndGetRandom被resolve,所以,fn函数的执行有效地暂停了 1 秒钟- 此外,
delayAndGetRandom返回一个随机数。无论在resolve函数中传递什么,它都被分配给变量b。 - 同样,变量
c值为 5 ,然后使用await delayAndGetRandom(1000)又延时了 1 秒钟。在这行代码中我们并没有使用Promise.resolve返回值。 - 最后,我们计算
a + b * c结果,并用Promise.resolve包裹并返回
注意:如果这里函数的暂停和恢复使你想起了 ES6 generators ,那是因为 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));
// Implementation One (Using for-loop)
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);
});
// Implementation Two (Using Recursion)
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);
});
});
};
如果使用async函数,更简单
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
错误处理
我们在语法部分所了解的,一个未捕获的 Error() 会被包装在一个 rejected promise 中,但是,我们可以在 async 函数中同步地使用 try-catch 处理错误。让我们从这一实用的函数开始 ——
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
canRejectOrReturn() 是一个异步函数,要么 resolve 'perfect number',要么 reject Error('Sorry, number too big')
看看下面的代码
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
因为我们在等待 canRejectOrReturn 执行,它的 rejection 会被转换为一个错误抛出,catch 会执行,也就是说 foo 函数结果要么 resolve 为 undefined(因为我们在 try 中没有返回值),要么 resolve 'error caught'。因为我们在 foo 函数中使用了 try-catch 处理错误,所以说 foo 函数的结果永远不会是 rejected。
另一个例子
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
注意。这次在 foo 函数里面我们返回而不是等待 canRejectOrReturn,foo 要么 resolve 'perfect number',要么 reject Error('Sorry, number too big') ,catch 语句不会被执行
因为我们 return 了 canRejectOrReturn 返回的 promise 对象,因此 foo 最终的状态由 canRejectOrReturn 的状态决定,你可以将 return canRejectOrReturn() 分成两行代码,来更清楚的了解,注意第一行没有 await
try {
const promise = canRejectOrReturn();
return promise;
}
让我们来看看 return 和 await 一起使用的情况
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
在这种情况下 foo resolve 'perfect number',或者 resolve 'error caught',没有 rejection,就像上面那个只有 await 的例子,在这里我们 resolve 了 canRejectOrReturn 返回的值,而不是 undefined
比也可以将 return await canRejectOrReturn() 拆分来看
try {
const value = await canRejectOrReturn();
return value;
}
// ...
常见的错误和陷阱
由于 Promise 和 async/await 之间错综复杂的操作。 可能会有一些隐藏的错误,我们来看看 -
没有使用 await
有时候我们会忘记在 promise 前面使用 await,或者忘记 return
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}
注意,如果我们不使用 await 或者 return,foo 总是会 resolve undefined,不会等待一秒,但是 canRejectOrReturn() 中的 promise 的确被执行了。如果有副作用,也会产生,如果抛出错误或者 reject,UnhandledPromiseRejectionWarning 就会产生
在回调中使用 async 函数
我们经常在 .map 和 .filter 中使用 async 函数作为回调函数,假设我们有一个函数 fetchPublicReposCount(username),可以获取一个 github 用户拥有的公开仓库的数量。我们想要获得三名不同用户的公开仓库数量,让我们来看代码 —
const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
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 实际上是一个 promises 数组,这个数组包含着每次调用函数获取用户仓库数量返回的 promise,
过度按顺序使用 await
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,并将它们保存到 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 时,返回值是一个包含所有结果的数组,而失败的时候则返回最先被reject失败状态的值,只要有一个 promise 对象被 rejected,Promise.all 的返回值为第一个被 rejected 的 promise 对象对应的返回值。但是,同时运行所有 promise 可能行不通。如果你想批量完成 promise。可以参考 p-map 关于数量可控的并发操作。
总结
async 函数非常重要。随着 Async Iterators 的引入,async 函数将会应用得越来越广。对于现代 JavaScript 开发人员来说掌握并理解 async 函数至关重要。希望这篇文章能对你有所帮助