【问题标题】:In JavaScript, does using await inside a loop block the loop?在 JavaScript 中,在循环中使用 await 会阻塞循环吗?
【发布时间】:2017-11-08 15:46:55
【问题描述】:

执行以下循环:

for(var i=0; i<100; ++i){
    let result = await some_slow_async_function();
    do_something_with_result();
}
  1. await 会阻塞循环吗?或者i 是否在awaiting 时继续递增?

  2. do_something_with_result() 的顺序是否保证相对于i 是连续的?还是取决于awaited 函数对每个i 的速度有多快?

【问题讨论】:

  • 你真的试过了吗?
  • 是的,我得到了一个连续的结果。我不确定这是否是巧合(异步功能实际上很快)。我不确定是否将所有异步函数调用推送到一个数组中,然后执行单个 await Promise.all(arr),或者这种形式是否正确并且其他东西阻碍了所需的异步性。如果我为所有这些都做一个await,那么我必须进入Promise.map 才能处理每一个。这让我怀疑.then 在这种情况下是否比 async/await 更好。
  • JS 是确定性的。该函数要么是异步的,要么不是,它从不依赖于“多快”的执行速度。关于Promise.all,这会有所不同——它是否仍然正确(或者更可取)取决于您的要求。
  • 在执行涉及外部资源的 async 操作时,函数运行的时间是不确定的,例如数据库,文件 i/o。
  • 是的,但是只要是异步的,它就永远不会立即调用它的回调,总是先运行到同步代码完成。

标签: javascript async-await


【解决方案1】:

正如@realbart 所说,它确实阻塞了循环,这将使调用按顺序进行。

如果你想触发大量等待操作,然后一起处理它们,你可以这样做:

const promisesToAwait = [];
for (let i = 0; i < 100; i++) {
  promisesToAwait.push(fetchDataForId(i));
}
const responses = await Promise.all(promisesToAwait);

【讨论】:

  • 这个答案实际上并没有回答问题
  • 这毫无意义。你的 promisesToAwait 数组永远不会包含承诺。
  • 它不会阻塞循环,但它也不会按预期执行。
  • realbart - 您参考谁的答案 - 删除了他们的答案,因为声称循环被阻止至少是误导,如果不是:只是错误的。什么都没有被阻止。执行继续,只是不在循环中。另外:“使调用顺序”:这不是很清楚:除了顺序执行 JS 代码没有其他方法。
【解决方案2】:
  1. await 会阻塞循环吗?或者i 是否在awaiting 时继续递增?

“阻塞”不是正确的词,但是是的,i 在等待时不会继续递增。相反,执行会跳回到调用async 函数的位置,提供一个promise 作为返回值,继续函数调用之后的其余代码,直到代码堆栈被清空。然后当等待结束时,函数的状态被恢复,并在该函数内继续执行。每当该函数返回(完成)时,相应的承诺(之前返回的)就会被解析。

  1. do_something_with_result() 的顺序是否保证相对于i 是连续的?还是取决于awaited 函数对每个i 的速度有多快?

订单有保证。 await 后面的代码也保证只在调用堆栈被清空后执行,即至少在下一个微任务可以执行时或之后执行。

看看这个sn-p的输出如何。特别注意它说“在调用测试之后”:

async function test() {
    for (let i = 0; i < 2; i++) {
        console.log('Before await for ', i);
        let result = await Promise.resolve(i);
        console.log('After await. Value is ', result);
    }
}

test().then(_ => console.log('After test() resolved'));

console.log('After calling test');

【讨论】:

  • 堆栈解释提醒我 async/await 直到最近才使用 generators/yield 实现。这样一想,一切就更清楚了。我会接受这个答案。
  • @smorgs 堆栈解释实际上更多地与标准 promise then 行为有关,而不是与生成器有关。只有“跳转回调用的地方”和“函数状态恢复”是yield
  • 明白了;我发现堆栈模型在.then 中很容易看到,但在await 中则不然,因此将其视为yield 可以解决我的困惑。
【解决方案3】:

您可以像这样在“FOR LOOP”中测试 async/await:

(async  () => {
        for (let i = 0; i < 100; i++) {
                await delay();
                console.log(i);
        }
})();

function delay() {
        return new Promise((resolve, reject) => {
                setTimeout(resolve, 100);
        });
}

【讨论】:

    【解决方案4】:

    await 会阻塞循环吗?还是i 在等待时继续递增?

    不,await 不会阻塞循环。是的,i 在循环时会继续递增。

    对于i,do_something_with_result() 的顺序是否保证顺序?还是取决于每个i 的等待函数有多快?

    保证do_something_with_result() 的顺序但不保证i。这取决于等待的函数运行的速度。

    some_slow_async_function() 的所有调用都是批处理的,即如果do_something_with_result()console,那么我们将看到它打印了循环运行的次数。然后依次执行所有的 await 调用。

    为了更好的理解你可以运行下面的代码sn-p:

    async function someFunction(){
    for (let i=0;i<5;i++){
     await callAPI();
     console.log('After', i, 'th API call');
    }
    console.log("All API got executed");
    }
    
    function callAPI(){
    setTimeout(()=>{
    console.log("I was called at: "+new Date().getTime())}, 1000);
    }
    
    someFunction();

    我们可以清楚地看到console.log('After', i, 'th API call'); 行是如何在整个 for 循环中首先打印出来的,然后在所有代码执行结束时,我们会从callAPI() 得到结果。

    因此,如果 await 之后的行依赖于从 await 调用获得的结果,那么它们将无法按预期工作。

    总而言之,for-loop 中的 await 并不能确保对从 await 调用 获得的结果进行成功操作,这可能需要一些时间才能完成。

    在节点中,如果使用neo-async 库和waterfall,可以实现这一点。

    【讨论】:

    • 这是因为您的 callAPI 函数没有返回承诺,所以 await callAPI() 立即返回。如果您将 callAPI() 更改为返回一个承诺,那么它将起作用。
    【解决方案5】:

    没有事件循环未被阻止,请参见下面的示例

    function sayHelloAfterSomeTime (ms) {
      return new Promise((resolve, reject) => {
        if (typeof ms !== 'number') return reject('ms must be a number')
        setTimeout(() => { 
          console.log('Hello after '+ ms / 1000 + ' second(s)')
          resolve()  
        }, ms)
      })
    }
    
    async function awaitGo (ms) {
       await sayHelloAfterSomeTime(ms).catch(e => console.log(e))
       console.log('after awaiting for saying Hello, i can do another things  ...')
    }
    
    function notAwaitGo (ms) {
    	sayHelloAfterSomeTime(ms).catch(e => console.log(e))
        console.log('i dont wait for saying Hello ...')
    }
    
    awaitGo(1000)
    notAwaitGo(1000)
    console.log('coucou i am event loop and i am not blocked ...')

    【讨论】:

      【解决方案6】:

      async 函数返回一个 Promise,它是一个最终将“解析”为一个值或“拒绝”并出现错误的对象。 await 关键字的意思是等到这个值(或错误)被最终确定。

      所以从运行函数的角度来看,它会阻塞等待慢速异步函数的结果。另一方面,javascript引擎看到这个函数被阻塞等待结果,所以它会去检查事件循环(即新的鼠标点击,或连接请求等),看看是否还有其他事情它可以一直工作直到返回结果。

      但是请注意,如果缓慢的异步函数很慢,因为它在您的 javascript 代码中计算大量内容,那么 javascript 引擎将没有大量资源来执行其他操作(并且通过执行其他操作可能会使慢异步功能甚至更慢)。异步函数的真正优势在于 I/O 密集型操作,例如查询数据库或传输大型文件,其中 javascript 引擎可以很好地真正等待其他东西(即数据库、文件系统等)。

      以下两段代码在功能上是等价的:

      let result = await some_slow_async_function();
      

      let promise = some_slow_async_function(); // start the slow async function
      // you could do other stuff here while the slow async function is running
      let result = await promise; // wait for the final value from the slow async function
      

      在上面的第二个例子中,慢速异步函数在没有await 关键字的情况下被调用,因此它将开始执行函数并返回一个promise。然后你可以做其他事情(如果你有其他事情要做)。然后await 关键字用于阻塞直到promise 真正“解决”。因此,从for 循环的角度来看,它将同步运行。

      所以:

      1. 是的,await 关键字的作用是阻塞 正在运行的函数,直到异步函数“解决”一个值或“拒绝”一个错误,但确实如此不阻塞javascript引擎,如果它在等待时有其他事情要做,它仍然可以做其他事情

      2. 是的,循环的执行将是顺序的

      http://javascript.info/async 上有一个很棒的教程。

      【讨论】:

      • “看到这个函数在等待结果时被阻塞了,所以它会去检查事件循环”:不,这是对所发生的事情的误解。当await 发生时,该函数实际上返回,并在函数调用后继续执行代码。
      • @trincot 细微差别特别是在函数调用之后继续执行“哪个”代码。是 1) 继续执行的异步函数中的代码,还是 2) 等待之后调用函数中的代码,或者 3) 排队到事件循环中的任何其他活动。从调用函数的角度来看,它似乎被阻塞了(等待 promise 解决),从异步函数的角度来看,它正在正常运行(但可能正在等待外部 I/O 或其他东西),以及从节点它将检查事件循环以查看是否还有其他事情要做。
      • @trincot 这也是为什么我说“具有阻止运行功能的'效果'”。它并没有真正被阻塞,Node 会从事件循环中做其他事情(以及通过执行异步函数继续工作),但从调用函数的角度来看(不是被调用的异步函数)它将“看起来”被阻止。
      • 提到 Node“将检查事件循环”是一种误导。当遇到await 时,节点将检查事件循环。该函数将返回,并且代码将继续执行,就像在 any 函数调用之后一样。事件循环仅在调用堆栈为空时起作用,而在遇到await 时则不起作用。顺便说一句:这个问题不是专门针对 Node 的,而是关于 JavaScript 的。
      • 你也写了“所以它会检查事件循环(即新的鼠标点击,或连接请求等)”。这不是真的。 (1) 代码执行继续 考虑事件循环,并且 (2) 即使有像鼠标点击这样的事件,它们也不优先于在不同队列中的承诺作业(“承诺作业队列”)。承诺解决方案(将在 await 处恢复函数上下文)优先于鼠标单击和其他代理驱动的事件。
      【解决方案7】:

      这是我对这个有趣问题的测试解决方案:

      import crypto from "crypto";
      
      function diyCrypto() {
          return new Promise((resolve, reject) => {
              crypto.pbkdf2('secret', 'salt', 2000000, 64, 'sha512', (err, res) => {
                  if (err) {
                      reject(err)
                      return 
                  }
                  resolve(res.toString("base64"))
              })
          })
      }
      
      setTimeout(async () => {
          console.log("before await...")
          const a = await diyCrypto();
          console.log("after await...", a)
      }, 0);
      
      setInterval(() => {
          console.log("test....")
      }, 200);
      

      在 setTimeout 的回调中,await 会阻止执行。但是setInterval 一直在运行,所以事件循环照常运行。

      【讨论】:

        【解决方案8】:

        让我澄清一下,因为这里的一些答案有一些关于 Promise 执行如何工作的错误信息,特别是与事件循环相关时。

        在本例中,await 将阻塞循环。 do_something_with_result() 不会被调用,直到 await 完成它的预定工作。

        https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await#handling_asyncawait_slowdown

        至于其他点,Promise“jobs”在下一个事件循环周期之前运行,如microtasks。当您调用Promise.then()resolve() 内部的resolve() 函数时,您将创建一个Jobawaitasync 都是 Promise 的包装器,它们都将创建一个 Job。微任务意味着在下一个事件循环周期之前运行。这意味着添加 Promise Job 意味着在进入下一个事件循环之前需要做更多的工作。

        这里有一个示例,您可以如何锁定您的事件循环,因为您的承诺(作业)耗时太长。

        let tick = 0;
        let time = performance.now();
        setTimeout(() => console.log('Hi from timeout'), 0);
        const tock = () => console.log(tick++);
        const longTask = async () => {
          console.log('begin task');
          for(let i = 0; i < 1_000_000_000; i++) {
            Math.sqrt(i);
          }
          console.log('done task');
        }
        requestAnimationFrame(()=> console.log('next frame after', performance.now() - time, 'ms'));
        async function run() {
          await tock();  
          await tock();
          await longTask(); // Will stall your UI
          await tock();   // Will execute even though it's already dropped frames
          await tock();   // This will execute too
        }
        run();
        // Promise.resolve().then(tock).then(tock).then(longTask).then(tock).then(tock);
        

        在这个示例中,总共创建了 5 个 Promise。 2 次调用 tock,1 次调用 longTask,然后 2 次调用 tock。所有 5 个都将在下一个事件循环之前运行。

        执行将是:

        • 开始执行JS
        • 执行普通脚本
        • 运行 5 个预定的 Promise 作业
        • 结束 JS 执行
        • 事件循环循环开始
        • 请求动画帧触发
        • 超时触发

        最后一行注释行是没有async/await 的调度,结果相同。

        基本上,除非你告诉你的 JS 执行它可以在哪里暂停,否则你将暂停下一个事件循环周期。您的 Promise 将继续在当前事件循环中运行,直到 它完成其调用堆栈。当你调用外部的东西(如fetch)时,它可能会使用让调用堆栈结束并有一个回调来解决待处理的 Promise。像这样:

        function waitForClick() {
          return new Promise((resolve) => {
            // Use an event as a callback;
            button.onclick = () => resolve();
            // Let the call stack finish by implicitly not returning anything, or explicitly returning `undefined` (same thing).
            // return undefined; 
          })
        }
        

        如果您有一个很长的作业要完成,请使用 Web Worker 运行它而不暂停,或者插入一些暂停,例如 setTimeout()setImmediate()

        重塑longTask函数,你可以这样做:

        const longTask = async () => {
          console.log('begin task');
          for(let i = 0; i < 1_000_000_000; i++)
            if (i && i % (10_000_000) === 0) {
              await new Promise((r) => setTimeout(r,0));
            }
            Math.sqrt(i);
          console.log('done task');
        }
        

        基本上,与其一次性完成 10 亿条记录,不如只执行 1000 万条,然后等到下一个事件 (setTimeout) 再运行下一个。这里的坏处是它更慢,因为你有多少交回事件循环。相反,您可以使用 requestIdleCallback() 更好,但仍然不如通过 Web Worker 的多线程。

        但请注意,仅在函数周围加上 awaitPromise.resolve().then() 对事件循环没有帮助。两者都会等到函数返回一个 Promise 或一个值,然后才停止事件循环。您可以通过检查您正在调用的函数是否立即返回 unresolved Promise 来进行测试。

        【讨论】:

        • "很多答案都是错误的" - 是吗?只有一个明显错误的答案,被多次否决。顺便说一句,“在下一个事件循环之前”这句话没有意义,只有一个事件循环。您的意思是“在事件循环的下一个迭代之前”吗?
        • @Bergi 每个提到 event loop 状态的答案都不会阻塞事件循环,否则 Javascript 将返回事件循环。它可以是误导性的(正如在其他答案的 cmets 中所指出的那样)或只是错误的。没有必要指指点点,所以我保持模糊。 “事件循环的下一个循环”或“当前事件循环循环”在技术上更正确,但听起来令人困惑,但我会编辑它。
        • 啊,你指的是这个。是的,你的答案更准确,但我不会错说“await 不会像同步函数那样阻塞主线程,因此它有机会返回事件循环”。
        • 顺便说一句,“当您显式构造 Promise (new Promise) 时,您正在添加一个将在当前事件循环周期结束时运行的作业”错了,executor函数立即运行
        • @Bergi 非常感谢!我改变了关于new Promise 的观点,即在调用resolve() 时安排工作,并使措辞听起来更友好一些。我还添加了一些参考。如果您看到任何其他内容,请告诉我。
        猜你喜欢
        • 2019-01-06
        • 2016-04-21
        • 1970-01-01
        • 2016-02-09
        • 1970-01-01
        • 1970-01-01
        • 2019-07-16
        相关资源
        最近更新 更多