async function

我们在第四章的“Generators + Promises”中提到过,generatoryield一个promise给一个类似运行器的工具,它会在promise完成时推进generator —— 有一个提案是要为这种模式提供直接的语法支持。让我们简要看一下这个被提出的特性,它称为async function

回想一下第四章中的这个generator的例子:

  1. run( function *main() {
  2. var ret = yield step1();
  3. try {
  4. ret = yield step2( ret );
  5. }
  6. catch (err) {
  7. ret = yield step2Failed( err );
  8. }
  9. ret = yield Promise.all([
  10. step3a( ret ),
  11. step3b( ret ),
  12. step3c( ret )
  13. ]);
  14. yield step4( ret );
  15. } )
  16. .then(
  17. function fulfilled(){
  18. // `*main()` 成功地完成了
  19. },
  20. function rejected(reason){
  21. // 噢,什么东西搞错了
  22. }
  23. );

被提案的async function语法可以无需run(..)工具就表达相同的流程控制逻辑,因为JS将会自动地知道如何寻找promise来等待和推进。考虑如下代码:

  1. async function main() {
  2. var ret = await step1();
  3. try {
  4. ret = await step2( ret );
  5. }
  6. catch (err) {
  7. ret = await step2Failed( err );
  8. }
  9. ret = await Promise.all( [
  10. step3a( ret ),
  11. step3b( ret ),
  12. step3c( ret )
  13. ] );
  14. await step4( ret );
  15. }
  16. main()
  17. .then(
  18. function fulfilled(){
  19. // `main()` 成功地完成了
  20. },
  21. function rejected(reason){
  22. // 噢,什么东西搞错了
  23. }
  24. );

取代function *main() { ..声明的,是我们使用async function main() { ..形式声明。而取代yield一个promise的,是我们await这个promise。运行main()函数的调用实际上返回一个我们可以直接监听的promise。这与我们从一个run(main)调用中拿回一个promise是等价的。

你看到对称性了吗?async function实质上是 generators + promises + run(..)模式的语法糖;它们在底层的操作是相同的!

如果你是一个C#开发者而且这种async/await看起来很熟悉,那是因为这种特性就是直接由C#的特性启发的。看到语言提供一致性是一件好事!

Babel、Traceur 以及其他转译器已经对当前的async function状态有了早期支持,所以你已经可以使用它们了。但是,在下一节的“警告”中,我们将看到为什么你也许还不应该上这艘船。

注意: 还有一个async function*的提案,它应当被称为“异步generator”。你可以在同一段代码中使用yieldawait两者,甚至是在同一个语句中组合这两个操作:x = await yield y。“异步generator”提案看起来更具变化 —— 也就是说,它返回一个没有还没有完全被计算好的值。一些人觉得它应当是一个 可监听对象(observable),有些像是一个迭代器和promise的组合。就目前来说,我们不会进一步探讨这个话题,但是会继续关注它的演变。

警告

关于async function的一个未解的争论点是,因为它仅返回一个promise,所以没有办法从外部 撤销 一个当前正在运行的async function实例。如果这个异步操作是资源密集型的,而且你想在自己确定不需要它的结果时能立即释放资源,这可能是一个问题。

举例来说:

  1. async function request(url) {
  2. var resp = await (
  3. new Promise( function(resolve,reject){
  4. var xhr = new XMLHttpRequest();
  5. xhr.open( "GET", url );
  6. xhr.onreadystatechange = function(){
  7. if (xhr.readyState == 4) {
  8. if (xhr.status == 200) {
  9. resolve( xhr );
  10. }
  11. else {
  12. reject( xhr.statusText );
  13. }
  14. }
  15. };
  16. xhr.send();
  17. } )
  18. );
  19. return resp.responseText;
  20. }
  21. var pr = request( "http://some.url.1" );
  22. pr.then(
  23. function fulfilled(responseText){
  24. // ajax 成功
  25. },
  26. function rejected(reason){
  27. // 噢,什么东西搞错了
  28. }
  29. );

我构想的request(..)有点儿像最近被提案要包含进web平台的fetch(..)工具。我们关心的是,例如,如果你想要用pr值以某种方法指示撤销一个长时间运行的Ajax请求会怎么样?

Promise是不可撤销的(在本书写作时)。在我和其他许多人看来,它们就不应该是可以被撤销的(参见本系列的 异步与性能)。而且即使一个proimse确实拥有一个cancel()方法,那么一定意味着调用pr.cancel()应当真的沿着promise链一路传播一个撤销信号到async function吗?

对于这个争论的几种可能的解决方案已经浮出水面:

  • async function将根本不能被撤销(现状)
  • 一个“撤销存根”可以在调用时传递给一个异步函数
  • 将返回值改变为一个新增的可撤销promsie类型
  • 将返回值改变为非promise的其他东西(比如,可监听对象,或带有promise和撤销能力的控制存根)

在本书写作时,async function返回普通的promise,所以完全改变返回值不太可能。但是现在下定论还是为时过早了。让我们持续关注这个讨论吧。