【问题标题】:Javascript async/await behaviour explanationJavascript async/await 行为解释
【发布时间】:2018-08-15 17:36:36
【问题描述】:

我一直认为 Javascipt 的 async/await 只是糖语法,例如编写这种和平的代码

let asyncVar = await AsyncFunc()
console.log(asyncVar)
//rest of code with asyncVar

相当于写这个

AsyncFunc().then(asyncVar => {
   console.log(asyncVar)
   //rest of the code with asyncVar
})

特别是因为你可以在一些随机的、无承诺的对象上调用 await 并且它会尝试调用它的 then() 函数

但是,我尝试了这种和平的代码

let asyncFunc = function() {
    return new Promise((resolve,reject) => {
       setTimeout(_ => resolve('This is async'), 1000)
    })
}

for(let i = 0; i < 5; i++) {
   console.log('This is sync')
   asyncFunc().then(val => console.log(val))
}
for(let i = 0; i < 5; i++) {
   console.log('This is sync')
   console.log(await asyncFunc())
}

第一个循环按预期输出 5 次“这是同步”,然后输出 5 次“这是异步”。 第二个循环输出“这是同步”、“这是异步”、“这是同步”、“这是异步”等。

谁能解释一下有什么区别,或者换句话说,async/await 在幕后到底做了什么?

【问题讨论】:

    标签: javascript async-await es6-promise


    【解决方案1】:

    你的前两个例子基本上是等价的。但是,你猜怎么着,你不能在这两个例子中使用for 循环。在for 循环中模拟await 语句没有一种简单的编码方式。这是因为for 循环(或while 循环)与async/await 的交互比在您的两个示例中插入简单的.then() 语句更先进。

    await 暂停整个包含函数的执行。这甚至会暂停 forwhile 循环。要仅使用 .then() 进行类似的编程,您必须发明自己的循环结构,因为仅使用 for 循环是无法做到的。

    例如,如果您使用await

    let asyncFunc = function() {
        return new Promise((resolve,reject) => {
           setTimeout(_ => resolve('This is async'), 1000)
        })
    }
    
    async function someFunc() {
        for(let i = 0; i < 5; i++) {
           console.log('This is sync')
           console.log(await asyncFunc())
        }
    }
    

    而且,如果您想仅使用.then() 进行类比,则不能使用for 循环,因为没有await 就无法“暂停”它。相反,您必须设计自己的循环,这通常涉及调用本地函数并维护自己的计数器:

    function someFunc() {
        let i = 0;
    
        function run() {
            if (i++ < 5) {
                console.log('This is sync')
                asyncFunc().then(result => {
                    console.log(result);
                    run();
                });
            }
        }
        run();
    }
    

    或者,有些人使用 .reduce() 这样的构造(尤其是在迭代数组时):

    // sequence async calls iterating an array
    function someFunc() {
        let data = [1,2,3,4,5];
        return data.reduce((p, val) => {
            console.log('This is sync')
            return p.then(() => {
               return asyncFunc().then(result => {
                    console.log(result);
                });
            });
        }, Promise.resolve());
    }
    

    谁能解释一下有什么区别,或者换句话说,async/await 在幕后到底做了什么?

    在幕后,Javascript 解释器会在 await 语句处暂停函数的进一步执行。保存当前的函数上下文(局部变量状态、执行点等),从函数返回一个承诺,并在该函数之外继续执行,直到稍后某个时间等待被等待的承诺解决或拒绝。如果它解决了,则选择相同的执行状态,并且该函数继续执行更多直到下一个 await 等等,直到最终该函数没有更多的 await 语句并且没有更多要执行(或到达return 声明)。

    为了进一步解释,这里有一些关于异步函数的背景:

    当您声明 async 函数时,解释器会创建一种始终返回承诺的特殊函数。如果函数显式返回一个被拒绝的 Promise 或函数中存在异常(异常被解释器捕获并更改为对函数返回的 Promise 的拒绝),则该 Promise 将被拒绝。

    如果/当函数返回一个正常值或只是通过完成其执行正常返回时,承诺就会被解决。

    当您调用该函数时,它会像任何普通函数一样开始同步执行。这包括函数中可能存在的任何循环(如示例中的 for 循环)。一旦函数遇到第一个await 语句并且它正在等待的值是一个promise,那么函数就会在该点返回并返回它的promise。该功能的进一步执行被暂停。由于该函数返回其承诺,因此该函数调用之后的任何代码都会继续运行。稍后,当 async 函数内部等待的承诺解决时,将在事件循环中插入一个事件以恢复该函数的执行。当解释器返回事件循环并到达该事件时,函数的执行将在await 语句之后的行上继续执行。

    如果有任何其他 await 语句正在等待 Promise,那么它们同样会导致函数执行暂停,直到正在等待的 Promise 被解决或拒绝。

    这种在过程中暂停或暂停函数执行的能力的美妙之处在于它可以在循环结构中工作,例如forwhile,这使得使用这样的循环按顺序执行异步操作非常重要在我们有 asyncawait 之前(正如您所发现的),更容易编程。仅使用.then() 来编写排序循环并没有简单的类似方法。有些使用.reduce(),如上所示。其他人使用内部函数调用,如上所示。

    【讨论】:

    • 感谢您提供如此详尽的回答。这个答案与之前的一个答案相似,但要澄清一下,您是说 async/await 不仅仅是一种语法糖,它实际上会暂停函数的执行?我知道它并没有真正的区别,它取决于解释器,但只是出于好奇(专注于 node.js)
    • @IvanJakab - 是的,它实际上暂停了函数的执行。对于外界,该函数在第一个await 处返回一个promise。在函数内部,执行会暂停,稍后会在等待的 promise 解决时恢复。 async/await 不仅仅是语法糖。正如我的代码示例所示,它允许您比使用.then() 更轻松地做一些事情。 await 不会阻塞解释器或事件循环。外部世界(在async 函数之外)认为该函数返回了一个未解决的承诺(因为它确实如此)。
    【解决方案2】:

    await 会按照包装盒上的说明进行操作:等待异步函数的响应:

    let asyncFunc = () =>
        new Promise((resolve,reject) => {
           setTimeout(() => resolve("This is async"), 5000)
        });
    
    console.log("before await");
    console.log(await asyncFunc());
    console.log("after await");
    

    这将记录"before await",然后,5 秒后,它将记录"This is async",然后是"after await"

    它基本上使异步函数的行为同步。

    【讨论】:

    • 我知道它的行为方式,但不幸的是,这并不能回答我的问题
    • @IvanJakab:那么你的问题到底是什么?它是如何实现的?
    【解决方案3】:

    async/await 是下一代的 Promise。基本上,await 将保留其余代码的执行,直到 await 解决。 例如。

    async function getUser() {    
         const id = 'zzzz';    
         const user = await get(`http://getuser.xxx/{id}`);
    
         console.log(user);    
    }
    

    get 函数执行后唯一打印的控制台。

    【讨论】:

      【解决方案4】:

      async/await 可能是语法糖,但它不仅仅是 then() 周围的糖。async/await 更好的类比是 generators + promises。 如果你编写一个生成器,你会注意到语法与async/await 几乎相同,但使用yield 而不是await。

      例如,您可以编写一个看起来几乎与您的第二个 for 循环完全相同的生成器,它会给您相同的行为:

      let asyncFunc = function() {
        return new Promise((resolve,reject) => {
           setTimeout(_ => resolve('This is async'), 1000)
        })
      }
      
      /*  Generator Function  */
      function *gen(){
        for(let i = 0; i < 5; i++) {
          console.log('This is sync')
          yield asyncFunc().then(console.log)  // looks and acts a lot like await
         }
      }
      
      /* iterate through the generator */
      let it = gen()
      function runGen(){
        let p = it.next()
        if (p.done) return
        else p.value.then(runGen)
      }
      runGen()

      【讨论】:

      • 但是@Cerbrus 确实如此。 await 在生成器中的工作方式非常类似于 yield。它会在函数的那个​​点暂停执行——就像 yield 一样。语法看起来一样;行为非常接近。问题是询问“幕后发生了什么”并试图从语法糖的角度来理解它。我认为这可能会有所帮助......显然不是每个人都这么认为。
      • 好吧,我想说的是,对于不知道 await 如何工作的人来说,生成器可能更加晦涩难懂。我认为您没有简化代码 ;-)
      • 我们清楚地以不同的方式阅读了这个问题@Cerbrus。看起来 OP 知道如何使用 async/await 对我来说很好。
      • 如何使用它,是的。但他明确地询问它是如何工作的“async/await 在幕后到底做了什么?” 我只是说,如果你是试图解释await 的作用。
      • generators 和 yield 对于大多数人来说可能不如 async/await 好理解,因此使用 yield 和 generators 来解释 async/await 并不会帮助很多人。这有点像用法语单词向讲英语的人解释西班牙语短语的含义。
      【解决方案5】:

      单独来看,async/await 是 Promise 的语法糖。你错过了更大的背景:承诺本身就在那里,因为 JS 无法在 await 出现之前暂停函数的执行。为了模拟这种暂停效果,您必须转换整个同步函数(在本例中为 for 循环),而不仅仅是 await 行。

      首先,我们必须将for 循环展开为while(说到语法糖!),然后我们可以将其重写为递归(因为在await 之前我们只能在函数之间“停止”,不在它们内部),那么我们可以重新连接它以仅在承诺履行时递归。这是从 async/await 转换为 Promise 时的代码:

      let asyncFunc = function() {
        return new Promise((resolve, reject) => {
          setTimeout(() => resolve('This is async'), 1000)
        });
      };
      
      function loop(i) {
        if (i < 5) {
          console.log('This is sync');
          return asyncFunc().then(result => {
            console.log(result);
            return loop(++i);
          });
        }
      }
      
      loop(0);

      你现在基本上有两种函数:async 和非async 函数,并且只有async 函数中可以有await(否则你会得到“SyntaxError: await is only valid在异步函数中”)。这是一种原因:为了将await 语义“重写”为非await 语义,您需要更改整个函数,而不仅仅是await 行。它是一种语法糖,但在 整个函数 级别,而不是在 OP 示例中的语句级别。

      编辑:您基本上也可以阅读原始代码中需要完成的操作:

      for(let i = 0; i < 5; i++) {
         console.log('This is sync')
         console.log(await asyncFunc())
      }
      

      让循环以i 开始,即0。检查i 是否小于5;如果是这样,记录“这是同步”,并调用asyncFunc(并等待它完成)。 然后,打印出结果,增加计数器并继续循环。

      尽管代码在 JavaScript 语法方面进行了相当大的重新排列,但听起来仍然很像我写的,不是吗? :)

      【讨论】:

      • 这其实解释了很多,但是你是说for循环其实是幕后的递归函数?这可以解释很多,因为我确实在某处读到 for(let i = 0; i
      • @IvanJakab:它们在幕后是什么完全取决于浏览器/引擎如何实现它们。
      • 不,在幕后它们可能不是递归函数。我写这个是为了说明创建了什么样的代码async/await 来替换,因为已经进行了语法扩展。准确了解幕后实际发生的事情的唯一方法是阅读浏览器源代码。要清楚地了解应该发生什么,唯一的方法是阅读 ECMAScript 规范(从 herehere 开始)。
      • 您似乎误解了人们使用“语法糖”这个短语只是意味着它是对承诺代码的简单搜索和替换。它不是。从我的示例中应该可以清楚地看出,async/await 严重影响了代码的结构,将一个视为另一个的宏是相当错误的。然而,正如你所看到的,总是可以用 promise 风格和 async/await 风格编写相同的东西,而后者显然更容易阅读——这就是人们称之为“语法糖”的原因,因为你可以重写以更容易理解的语法实现相同的语义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-25
      • 1970-01-01
      • 2019-08-11
      • 1970-01-01
      • 2018-10-29
      • 2018-12-15
      相关资源
      最近更新 更多