ES2017 - 异步 vs. Yield

34

关于添加异步函数和关键字await到下一个EcmaScript的讨论,我感到困惑。

我不明白为什么在function关键字之前需要有async关键字。

在我看来,await关键字等待生成器或承诺已完成的结果,函数的return应该足够了。

await应该可以在普通函数和生成器函数中简单地使用,而无需其他async标记。

如果我需要创建一个可以用作await结果的函数,我可以简单地使用一个promise。

我提出疑问的原因是这篇好文章,其中包含以下示例:

async function setupNewUser(name) {  
  var invitations,
      newUser = await createUser(name),
      friends = await getFacebookFriends(name);

  if (friends) {
    invitations = await inviteFacebookFriends(friends);
  }

  // some more logic
}

如果函数的执行需要等待所有的await关键字都被满足后才能完成,则也可以将其作为常规函数来执行。

function setupNewUser(name) {  
  var invitations,
      newUser = await createUser(name),
      friends = await getFacebookFriends(name);

  if (friends) {
    invitations = await inviteFacebookFriends(friends);
  }

  // return because createUser() and getFacebookFriends() and maybe inviteFacebookFriends() finished their awaited result.

}

我认为整个函数执行会一直等待直到下一个tick(等待结果)完成。与Generator-Function的区别在于,next()触发并更改对象的值和done字段。相反,函数将在完成时仅返回结果,并且触发器是函数内部触发器,例如while循环。


2
如果您阻止了该函数,不仅是此函数被阻止,而且调用此函数的函数,以及整个调用栈都将被阻止,这与JS当前的工作方式有很大的区别。标记为async的函数返回值的promise以避免这种情况,因为调用者将继续执行并最终从promise结果获取结果。 - loganfsmyth
2
不确定你的困惑是否也涉及到async和生成器函数之间的区别? - Bergi
1
@Bergi:是的,我认为我没有理解异步将在对象树中创建什么。生成器我很了解。异步对我来说让我困惑,因为它看起来没有必要区分(参见while循环)。但感谢您下面的回答 - 我也发表了评论... - Danny
1
@loganfsmyth:是的,那很有道理。由于LIFO,while循环会阻塞事件循环之外的所有内容。异步将在事件循环内部组织(这是FIFO吗)? - Danny
@Danny:你知道 Promise 怎么工作吗?如果不知道,最好先学习一下它们,然后再尝试理解 async-await 语法。 - Bergi
1
@Bergi:是的,我知道 - 我试图理解内部机制(引擎底层发生了什么)把自己完全搞糊涂了。最初的问题是关于异步单词的需求。现在我知道它是关于创建何种类型的对象(包括其方法)。 - Danny
4个回答

22
我不明白为什么在函数关键字之前需要有async关键字。
和生成器函数之前需要*号一样,这是为了标记此函数是非同寻常的。它们在这方面非常相似——它们添加了一个视觉标记,表明此函数的主体不能自行完成,但可以与其他代码任意交错执行。
*号表示生成器函数,它将始终返回一个生成器,可以通过消耗它类似于迭代器来推进(和停止)。
async表示异步函数,它将始终返回依赖于其他承诺的承诺,并且其执行与其他异步操作并发(并可能从外部取消)。
虽然关键字并不是必需的,而函数的类型可以通过其主体中是否出现相应的关键字(yield(*)/await)来确定,但这会导致代码不易维护:
- 难以理解,因为您需要扫描整个主体才能确定其类型 - 更容易出错,因为很容易通过添加/删除这些关键字来破坏函数,而不会出现语法错误

一个普通的函数,其执行将等待整个主体完成,直到所有的等待都被满足。

这听起来像是你想要一个阻塞函数,在并发设置中这是一个非常糟糕的想法


1
谢谢 - 最终的原因是:它引导了创建的对象。一个生成器对象将具有值和完成状态,一个简单的函数将是函数对象,而异步函数将创建一个完成状态,没有值但有一个等待数组。JS引擎将通过流控制处理并记录这种类型的标志吗? - Danny
1
嗯,不是这样的。生成器对象将具有nextreturnthrow方法(它们返回.done/.value对),而Promise将具有一个then方法(它回调一个两个值中的一个)。不确定您所说的“标志”或“等待数组”的含义。主要区别在于,当从外部调用生成器代码时(如果有的话,且相当随意),它会从yield恢复,而异步代码则会在当前等待的Promise解决时从await恢复。 - Bergi
有没有比console.log(object)更好的跟踪状态的方法?我尝试通过创建一些序列来理解这个问题,但是引用的对象是空的。在生成器的情况下,如果我使用console.log(generator.next());,我会得到{value: "foo", done: false} - 但我想看到整个生成器以了解底层发生了什么。 - Danny
你可能想要执行 console.log(generator) 并且在生成器函数中放置日志,放在 yield 之间。不过我认为没有办法检查生成器的当前状态。 - Bergi
我之前尝试过这两种方法:var gen =function* generator(){ console.log(gen);yield ...}; 但正如你所说,这并没有帮助。 - Danny
不要将生成器函数与生成器实例本身混淆。我认为http://davidwalsh.name/es6-generators解释得很清楚。 - Bergi

20
通过将一个函数标记为async,你告诉JS始终返回一个Promise。
因为它总是返回一个Promise,所以它也可以在自己的块内部等待Promise。把它想象成一个巨大的Promise链——函数内部发生的事情会被有效地插入到其内部的.then()块中,返回的是链中最后一个.then()
例如,这个函数...
async function test() {
  return 'hello world';
}

...返回一个Promise。因此,您可以像执行Promise一样执行它,包括.then()等操作。

test().then(message => {
  // message = 'hello world'
});

所以...

async function test() {
  const user = await getUser();
  const report = await user.getReport();
  report.read = true
  return report;
}

大致类似于...

function test() {
  return getUser().then(function (user) {
    return user.getReport().then(function (report) {
      report.read = true;
      return report;
    });
  });
}

在这两种情况下,传递给test().then()的回调将作为其第一个参数接收report
生成器(即将函数标记为*并使用yield关键字)是完全不同的概念。它们不使用Promises。它们有效地允许您在代码的不同部分之间“跳转”,从函数内部产生结果,然后跳回该点并恢复到下一个yield块。
尽管它们感觉有些相似(即“停止”执行直到其他地方发生了某些事情),但async/await只是给您这种幻觉,因为它干扰了Promise执行的内部排序。它实际上并没有等待 - 它只是在调用回调函数的时间上进行了调整。
相比之下,生成器的实现方式不同,使得生成器可以维护状态并进行迭代。同样与Promises无关。
该行进一步模糊,因为在撰写本文时,对于async/await的支持很少;Chakracore原生支持它,而V8即将支持。同时,像Babel这样的转换器允许您编写async/await并将代码转换为生成器。得出结论生成器和async/await是相同的是错误的;它们不是...只是碰巧可以通过混合使用yield和Promises来获得类似的结果。
更新:2017年11月
Node LTS现在原生支持async/await,因此您无需使用生成器来模拟Promises。

2
非常勤奋的回答 - prosti
1
你真的为我解决了疑惑。co使用生成器,现在我明白了,当它被创建时async/await还不可用(除非我理解错了)。 - Camilo Martin
我在阅读这篇博客文章:https://blog.jscrambler.com/introduction-to-koajs/ (关于koa.js库),我想知道为什么示例广泛使用生成器而不使用async/await(这让我对两者的本质感到困惑 - 我是新手)。您的回答似乎暗示着如果支持async/await,则应将文章中使用的语法视为“过时的”。 - Nicolas Le Thierry d'Ennequin
1
是的,没错 - 那是 Koa v1。Koa 2 使用 Promises。 - Lee Benson

11

这些答案都提供了async关键字是好的原因的有效论点,但它们没有实际提到为什么必须将其添加到规范中的真正原因。

原因在于在 ES7 之前,这是一个有效的 JS。

function await(x) {
  return 'awaiting ' + x
}

function foo() {
  return(await(42))
}

根据你的逻辑,foo()会返回Promise{42}还是"awaiting 42"?(返回一个 Promise 将会破坏向后兼容性)

因此答案是:await是一个普通的标识符,在异步函数内部才被视为关键字,因此它们必须用某种方式标记。


1
await(42) 可能只是无效的 ES7 语法。我们本可以强制不使用括号,而是像实际上每个人都在做的那样使用 await 42。一旦你掉进这个陷阱,被迫在每个函数上标记 async 真的很糟糕,而且不能在返回 Promise 的常规函数中使用 await 更是另一个问题。它本可以比现在好得多... - DanielM
或许 await 需要接受括号语法,以允许自执行函数 await (function(){return promisify();})(); - Kernel James
1
当然,为了实现向后兼容性,需要关键字。但 async 不仅仅是一个可以创建新作用域以自由使用 await 的关键字,它改变了函数执行的基本方式,可能带来很多意想不到的副作用。 - Petruza
但是,如果我们计算一下旧项目中有多少个名为 await 的符号,并将其除以某个度量单位,比如 KLOC,那么它是否会成为一个非常重要的维护问题呢? - Petruza
这个答案已经被引用在这里 - Gras Double

3

在函数前面加上async关键字的原因很简单,这样你就知道返回值会被转换成一个Promise对象。如果没有这个关键字,解释器怎么知道要这样做呢?我认为这最初是在C#中引入的,而EcmaScript从TypeScript中借鉴了很多东西。TypeScript和C#都是由Anders Hejlsberg设计的,它们非常相似。假设你有一个函数(这只是为了进行一些异步工作)。

 function timeoutPromise() {  
     return (new Promise(function(resolve, reject) {
         var random = Math.random()*1000;
         setTimeout(
             function() {
                 resolve(random);
             }, random);
     }));
 }

这个函数将让我们等待一个随机时间并返回一个 Promise 对象(如果你使用 jQuery,Promise 类似于 Deferred)。要使用这个函数,你可以像这样编写代码:

function test(){
    timeoutPromise().then(function(waited){
        console.log('I waited' + waited);
    });
}

这很好。现在让我们尝试返回日志消息。

function test(){
    return timeoutPromise().then(function(waited){
        var message = 'I waited' + waited;
        console.log(message);
        return message; //this is where jQuery Deferred is different then a Promise and better in my opinion
    });
}

这不错,但是代码中有两个返回语句和一个函数。

现在使用异步编程,代码将如下所示:

  async function test(){
      var message = 'I waited' +  (await timeoutPromise());
      console.log(message);
      return message;
  }

代码短小且内联。如果你写了很多.then()或.done(),你就知道代码可能变得不可读。

现在为什么在函数前面加上async关键字?这是为了指示返回值不是实际被返回的值。理论上,你可以像这样编写代码(在C#中可以这样做,但我不确定JavaScript是否允许,因为它没有这样做)。

 async function test(wait){
     if(wait == true){
         return await timeoutPromise();
     }
     return 5;                
 }

你看,虽然你返回的是一个数字,但实际上返回的将是一个Promise。你不必使用 return new Promise(function(resolve, reject) { resolve(5);};,因为你不能等待一个数字,只能等待一个Promise。如果你不在前面加上async,await test(false)会抛出异常,而await test(true)则不会。


1
你的第一个test函数好像缺少了return语句?我不明白为什么jQuery Deferred比正确的Promise更好,它们返回值的工作方式基本相同。 - Bergi
timeoutPromise返回(新的Promise)。我不明白jQuery延迟对象为什么更好。 - Filip Cordas
你的第一个测试函数似乎缺少返回值(return)?timeoutPromise返回 (new Promise)。我不明白为什么jQuery deferreds更好?您有更多的函数,如allways、done、progress。链接 - Filip Cordas
timeoutPromise 会返回一些东西,但是 test 的第一个实现并没有。关于延迟对象(deferreds),jQuery 差劲且不符合标准always 相当琐碎done 没有用,而 progress 已经在所有主要库中被弃用了,有很好的理由。 - Bergi
@Bergi jQuery 3.0修复了它们的Promise并使其符合Promises/A+规范。 - Flimm
@Flimm 对于那些已经使用jQuery 3的人来说很好...但是他们的API仍然比简单的ES6实现更糟糕。 - Bergi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接