ECMAScript 2017 的特性“异步函数”是由 Brian Terlson 提出的。
异步函数存在以下几种变体。请注意,所有变体中都使用了关键字 async
。
async function foo() {}
const foo = async function () {};
let obj = { async foo() {} }
const foo = async () => {};
实现异步函数的 Promise
async
function
asyncFunc
()
{
return
123
;
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
x
));
// 123
拒绝异步函数的 Promise
async
function
asyncFunc
()
{
throw
new
Error
(
'Problem!'
);
}
asyncFunc
()
.
catch
(
err
=>
console
.
log
(
err
));
// Error: Problem!
await
处理异步计算的结果和错误 运算符 await
(仅允许在异步函数内部使用)会等待其操作数(一个 Promise)完成。
await
的结果是实现值。await
会抛出拒绝值。处理单个异步结果
async
function
asyncFunc
()
{
const
result
=
await
otherAsyncFunc
();
console
.
log
(
result
);
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc
()
.
then
(
result
=>
{
console
.
log
(
result
);
});
}
顺序处理多个异步结果
async
function
asyncFunc
()
{
const
result1
=
await
otherAsyncFunc1
();
console
.
log
(
result1
);
const
result2
=
await
otherAsyncFunc2
();
console
.
log
(
result2
);
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc1
()
.
then
(
result1
=>
{
console
.
log
(
result1
);
return
otherAsyncFunc2
();
})
.
then
(
result2
=>
{
console
.
log
(
result2
);
});
}
并行处理多个异步结果
async
function
asyncFunc
()
{
const
[
result1
,
result2
]
=
await
Promise
.
all
([
otherAsyncFunc1
(),
otherAsyncFunc2
(),
]);
console
.
log
(
result1
,
result2
);
}
// Equivalent to:
function
asyncFunc
()
{
return
Promise
.
all
([
otherAsyncFunc1
(),
otherAsyncFunc2
(),
])
.
then
([
result1
,
result2
]
=>
{
console
.
log
(
result1
,
result2
);
});
}
处理错误
async
function
asyncFunc
()
{
try
{
await
otherAsyncFunc
();
}
catch
(
err
)
{
console
.
error
(
err
);
}
}
// Equivalent to:
function
asyncFunc
()
{
return
otherAsyncFunc
()
.
catch
(
err
=>
{
console
.
error
(
err
);
});
}
在解释异步函数之前,我需要解释如何结合 Promise 和生成器,通过类似同步的代码来执行异步操作。
对于异步计算其一次性结果的函数,ES6 中的 Promise 已变得流行起来。一个例子是 客户端 fetch
API,它是 XMLHttpRequest 的替代方案,用于检索文件。使用它的方式如下所示
function
fetchJson
(
url
)
{
return
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
text
=>
{
return
JSON
.
parse
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
});
}
fetchJson
(
'http://example.com/some_file.json'
)
.
then
(
obj
=>
console
.
log
(
obj
));
co 是一个使用 Promise 和生成器来实现更像同步编码风格的库,但其工作原理与上一个示例中使用的风格相同
const
fetchJson
=
co
.
wrap
(
function
*
(
url
)
{
try
{
let
request
=
yield
fetch
(
url
);
let
text
=
yield
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
});
每次回调(一个生成器函数!)将 Promise 传递给 co 时,回调都会被挂起。一旦 Promise 完成,co 就会恢复回调:如果 Promise 被实现,则 yield
返回实现值;如果被拒绝,则 yield
抛出拒绝错误。此外,co 会将回调返回的结果 Promise 化(类似于 then()
的方式)。
异步函数基本上是 co 所做工作的专用语法
async
function
fetchJson
(
url
)
{
try
{
let
request
=
await
fetch
(
url
);
let
text
=
await
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
}
在内部,异步函数的工作方式与生成器非常相似。
以下是异步函数的执行方式
p
。该 Promise 在开始执行异步函数时创建。return
或 throw
永久结束。或者可以通过 await
暂时结束;在这种情况下,执行通常会在稍后继续。p
。在执行异步函数体时,return x
使用 x
实现 Promise p
,而 throw err
使用 err
拒绝 p
。完成通知是异步发生的。换句话说:then()
和 catch()
的回调总是在当前代码执行完毕后执行。
以下代码演示了它是如何工作的
async
function
asyncFunc
()
{
console
.
log
(
'asyncFunc()'
);
// (A)
return
'abc'
;
}
asyncFunc
().
then
(
x
=>
console
.
log
(
`Resolved:
${
x
}
`
));
// (B)
console
.
log
(
'main'
);
// (C)
// Output:
// asyncFunc()
// main
// Resolved: abc
您可以依赖以下顺序
return
实现。实现 Promise 是一个标准操作。return
使用它来实现异步函数的 Promise p
。这意味着
p
。p
现在反映了该 Promise 的状态。因此,您可以返回一个 Promise,并且该 Promise 不会被包装在另一个 Promise 中
async
function
asyncFunc
()
{
return
Promise
.
resolve
(
123
);
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
x
))
// 123
有趣的是,返回一个被拒绝的 Promise 会导致异步函数的结果被拒绝(通常,您会使用 throw
来实现这一点)
async
function
asyncFunc
()
{
return
Promise
.
reject
(
new
Error
(
'Problem!'
));
}
asyncFunc
()
.
catch
(
err
=>
console
.
error
(
err
));
// Error: Problem!
这与 Promise 实现的工作原理一致。它使您能够转发另一个异步计算的实现和拒绝,而无需使用 await
async
function
asyncFunc
()
{
return
anotherAsyncFunc
();
}
前面的代码与以下代码大致相似(但效率更高),后者解包 anotherAsyncFunc()
的 Promise 只是为了再次包装它
async
function
asyncFunc
()
{
return
await
anotherAsyncFunc
();
}
await
的技巧 await
在异步函数中,一个容易犯的错误是在进行异步函数调用时忘记 await
async
function
asyncFunc
()
{
const
value
=
otherAsyncFunc
();
// missing `await`!
···
}
在此示例中,value
被设置为 Promise,这通常不是您在异步函数中想要的。
即使异步函数没有返回任何内容,await
也有意义。然后,它的 Promise 只是用作信号,告诉调用者它已完成。例如
async
function
foo
()
{
await
step1
();
// (A)
···
}
第 (A) 行中的 await
确保在执行 foo()
的其余部分之前,step1()
已完全完成。
await
有时,您只想触发异步计算,而并不关心它何时完成。以下代码就是一个例子
async
function
asyncFunc
()
{
const
writer
=
openFile
(
'someFile.txt'
);
writer
.
write
(
'hello'
);
// don’t wait
writer
.
write
(
'world'
);
// don’t wait
await
writer
.
close
();
// wait for file to close
}
在这里,我们不关心单个写入何时完成,只关心它们是否按正确的顺序执行(API 必须保证这一点,但异步函数的执行模型鼓励这样做,正如我们所见)。
asyncFunc()
最后一行中的 await
确保仅在文件成功关闭后才实现该函数。
鉴于返回的 Promise 不会被包装,您也可以使用 return
而不是 await
writer.close()
async
function
asyncFunc
()
{
const
writer
=
openFile
(
'someFile.txt'
);
writer
.
write
(
'hello'
);
writer
.
write
(
'world'
);
return
writer
.
close
();
}
这两种版本都有优缺点,await
版本可能更容易理解。
await
是顺序的,Promise.all()
是并行的 以下代码进行了两次异步函数调用,asyncFunc1()
和 asyncFunc2()
。
async
function
foo
()
{
const
result1
=
await
asyncFunc1
();
const
result2
=
await
asyncFunc2
();
}
但是,这两个函数调用是顺序执行的。并行执行它们往往会加快速度。您可以使用 Promise.all()
来实现这一点
async
function
foo
()
{
const
[
result1
,
result2
]
=
await
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
]);
}
现在,我们不是等待两个 Promise,而是等待一个包含两个元素的数组的 Promise。
异步函数的一个限制是 await
只影响直接包围它的异步函数。因此,异步函数不能在回调中使用 await
(但是,回调本身可以是异步函数,我们将在后面看到)。这使得基于回调的实用函数和方法难以使用。例如数组方法 map()
和 forEach()
。
Array.prototype.map()
让我们从数组方法 map()
开始。在以下代码中,我们想下载 URL 数组指向的文件,并将它们返回到一个数组中。
async
function
downloadContent
(
urls
)
{
return
urls
.
map
(
url
=>
{
// Wrong syntax!
const
content
=
await
httpGet
(
url
);
return
content
;
});
}
这不起作用,因为 await
在普通箭头函数中是非法的语法。那么,使用异步箭头函数呢?
async
function
downloadContent
(
urls
)
{
return
urls
.
map
(
async
(
url
)
=>
{
const
content
=
await
httpGet
(
url
);
return
content
;
});
}
这段代码有两个问题
map()
完成后,回调执行的工作还没有完成,因为 await
只会暂停包围它的箭头函数,而 httpGet()
是异步实现的。这意味着您不能使用 await
等待 downloadContent()
完成。我们可以通过 Promise.all()
解决这两个问题,它将 Promise 数组转换为数组的 Promise(使用 Promise 实现的值)
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
async
(
url
)
=>
{
const
content
=
await
httpGet
(
url
);
return
content
;
});
return
await
Promise
.
all
(
promiseArray
);
}
map()
的回调对 httpGet()
的结果没有做太多处理,只是转发了它。因此,我们这里不需要异步箭头函数,普通箭头函数就可以了
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
url
=>
httpGet
(
url
));
return
await
Promise
.
all
(
promiseArray
);
}
我们还可以做一个小小的改进:这个异步函数效率有点低——它首先通过 await
解包 Promise.all()
的结果,然后再通过 return
包装它。鉴于 return
不会包装 Promise,我们可以直接返回 Promise.all()
的结果
async
function
downloadContent
(
urls
)
{
const
promiseArray
=
urls
.
map
(
url
=>
httpGet
(
url
));
return
Promise
.
all
(
promiseArray
);
}
Array.prototype.forEach()
让我们使用数组方法 forEach()
来记录 URL 指向的多个文件的内容
async
function
logContent
(
urls
)
{
urls
.
forEach
(
url
=>
{
// Wrong syntax
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
});
}
同样,这段代码会产生语法错误,因为您不能在普通箭头函数中使用 await
。
让我们使用异步箭头函数
async
function
logContent
(
urls
)
{
urls
.
forEach
(
async
url
=>
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
});
// Not finished here
}
这确实有效,但有一个需要注意的地方:httpGet()
返回的 Promise 是异步实现的,这意味着当 forEach()
返回时,回调还没有完成。因此,您不能等待 logContent()
的结束。
如果这不是您想要的,您可以将 forEach()
转换为 for-of
循环
async
function
logContent
(
urls
)
{
for
(
const
url
of
urls
)
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
}
}
现在,所有内容都在 for-of
循环结束后完成。但是,处理步骤是顺序发生的:只有在第一次调用完成后,才会第二次调用 httpGet()
。如果您希望处理步骤并行发生,则必须使用 Promise.all()
async
function
logContent
(
urls
)
{
await
Promise
.
all
(
urls
.
map
(
async
url
=>
{
const
content
=
await
httpGet
(
url
);
console
.
log
(
content
);
}));
}
map()
用于创建 Promise 数组。我们对它们实现的结果不感兴趣,我们只 await
直到它们都实现为止。这意味着我们在该异步函数结束时就完全完成了。我们也可以返回 Promise.all()
,但这样函数的结果将是一个所有元素都为 undefined
的数组。
异步函数的基础是 Promise。这就是为什么理解后者对于理解前者至关重要。特别是当将未基于 Promise 的旧代码与异步函数连接时,您通常别无选择,只能直接使用 Promise。
例如,这是 XMLHttpRequest
的“Promise 化”版本
function
httpGet
(
url
,
responseType
=
""
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
const
request
=
new
XMLHttpRequest
();
request
.
onload
=
function
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
};
request
.
onerror
=
function
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
};
request
.
open
(
'GET'
,
url
);
xhr
.
responseType
=
responseType
;
request
.
send
();
});
}
XMLHttpRequest
的 API 基于回调。通过异步函数对其进行 Promise 化意味着您必须从回调内部实现或拒绝函数返回的 Promise。这是不可能的,因为您只能通过 return
和 throw
来实现。而且您不能从回调内部 return
函数的结果。throw
也有类似的限制。
因此,异步函数的常见编码风格是
延伸阅读:“Exploring ES6”中的“用于异步编程的 Promise”一章。
有时,如果能够在模块或脚本的顶层使用 await
会很方便。可惜的是,它只能在异步函数内部使用。因此,您有几种选择。您可以创建一个异步函数 main()
并在之后立即调用它
async
function
main
()
{
console
.
log
(
await
asyncFunction
());
}
main
();
或者,您可以使用立即调用的异步函数表达式
(
async
function
()
{
console
.
log
(
await
asyncFunction
());
})();
另一种选择是立即调用的异步箭头函数
(
async
()
=>
{
console
.
log
(
await
asyncFunction
());
})();
以下代码使用 测试框架 mocha 对异步函数 asyncFunc1()
和 asyncFunc2()
进行单元测试
import
assert
from
'assert'
;
// Bug: the following test always succeeds
test
(
'Testing async code'
,
function
()
{
asyncFunc1
()
// (A)
.
then
(
result1
=>
{
assert
.
strictEqual
(
result1
,
'a'
);
// (B)
return
asyncFunc2
();
})
.
then
(
result2
=>
{
assert
.
strictEqual
(
result2
,
'b'
);
// (C)
});
});
但是,此测试始终成功,因为 mocha 不会等到第 (B) 行和第 (C) 行中的断言执行完毕。
您可以通过返回 Promise 链的结果来解决此问题,因为 mocha 会识别测试是否返回 Promise,然后等待该 Promise 完成(除非超时)。
return
asyncFunc1
()
// (A)
方便的是,异步函数始终返回 Promise,这使得它们非常适合这种单元测试
import
assert
from
'assert'
;
test
(
'Testing async code'
,
async
function
()
{
const
result1
=
await
asyncFunc1
();
assert
.
strictEqual
(
result1
,
'a'
);
const
result2
=
await
asyncFunc2
();
assert
.
strictEqual
(
result2
,
'b'
);
});
因此,在 mocha 中使用异步函数进行异步单元测试有两个优点:代码更简洁,并且也处理了返回 Promise 的问题。
JavaScript 引擎在警告未处理的拒绝方面越来越出色。例如,以下代码在过去通常会静默失败,但现在大多数现代 JavaScript 引擎都会报告未处理的拒绝
async
function
foo
()
{
throw
new
Error
(
'Problem!'
);
}
foo
();