JavaScript中的Promises和async/await行为。并发模式

3
我试图避免异步函数的并发,以便在再次调用相同函数之前等待第一次调用完成:

const disallowConcurrency = (fn) => {
  let inprogressPromise = Promise.resolve();

  return async(...args) => {
    await inprogressPromise;
    inprogressPromise = inprogressPromise.then(() => fn(...args));
  };
};

const initCmp = async(arg) => {
  return new Promise((res) => {
    console.log('executed');
    setTimeout(() => res(console.log(arg)), 1000);
  });
};
const cmpConcurrentFunction = disallowConcurrency(initCmp);
cmpConcurrentFunction('I am called 1 second later');
cmpConcurrentFunction('I am called 2 seconds later');

这里我创建了一个闭包,其中将一个promise作为传递给内部函数的值。

返回的函数将等待上一次调用完成(在第一次运行中,它是一个已解决的Promise),并将由then返回的Promise分配给inprogressPromise。 在这种情况下,then返回的将是通过initCmp函数返回的新Promise。 因此,下一次调用该函数时,它将等待上一个完成。至少这是我的理解。 我理解得对吗?

但是,如果我删除await inprogressPromise,例如像这样:

    const disallowConcurrency = (fn) => {
  let inprogressPromise = Promise.resolve();

  return async (...args) => {
    inprogressPromise = inprogressPromise.then(() => fn(...args));
  };
};

所以不需要使用await吗?为什么?

此外,我期望这个能够工作:

 const disallowConcurrency = (fn) => {
  let inprogressPromise = Promise.resolve();

  return async (...args) => {
    await inprogressPromise;
    inprogressPromise = fn(...args);
  };
};

因为我认为首先我要等待之前的承诺完成,然后我才会调用并将返回的承诺分配给inprogressPromise

但它不起作用,这两个函数同时被调用。

有人能给我解释一下这里发生了什么吗?

如果你尝试这段代码,你会看到第二次调用。

cmpConcurrentFunction("I am called 2 seconds later")

等待第一个promise完成。

基本上我编写了一个Node包,通过npm在Web应用程序中被导入到浏览器中。 这个Node包库有一个init函数,其中包含一些异步代码,并且可以在代码的不同部分多次调用。 例如,如果第二次调用它,我希望在尝试再次执行代码之前先确保第一次执行完成。

但我的问题是尝试理解为什么该代码有效,如果我使用最新版本则为什么无效。

谢谢!


2
async/await is just syntactic sugar that "hides" a Promise + .then() - Andreas
1
你正在等待一个已经被解决的Promise。这是什么意思? - Andreas
我想要实现的是,我可能会调用相同的异步函数两次,并且需要确保在再次调用该函数之前已经完成了第一次调用。基本上,我有一个带有init函数的库。这个init函数正在执行一个异步任务,这个库可以被导入到代码的不同部分,并且可以多次调用init函数。如果发生这种情况,我需要确保在再次调用它之前先完成上一次执行。我分享的示例有效,因为第二个函数调用等待第一个函数完成后才执行。 - Miriam
这是一个通过npm导入并在React上执行的节点库。 - Miriam
我认为你的意思是第一个片段中的代码实际上是有效的?如果是这样,我感到惊讶,因为您没有等待对cmpConcurrentFunction的任何调用。 - Robin Zigmond
显示剩余3条评论
1个回答

3

为了回答你的问题,让我先尝试解释一下你的代码是如何工作的。

了解代码的工作原理

以下步骤解释了你的代码的执行过程:

  1. Script execution start

  2. Call disallowConcurrency function, passing in the initCmp as an argument. cmpConcurrentFunction is assigned the return value of disallowConcurrency function

  3. Call cmpConcurrentFunction for the first time, passing in 'I am called 1 second later' as an argument. During this invocation, inprogressPromise is a resolved promise returned by Promise.resolve(). Awaiting it pauses the function execution.

  4. Call cmpConcurrentFunction for the second time, passing in 'I am called 2 seconds later' as an argument. During this second invocation, inprogressPromise is still a resolved promise returned by Promise.resolve(). Awaiting it pauses the function execution.

  5. Synchronous execution of script ends here. Event loop can now start processing the micro-task queue

  6. Function paused as a result of first invocation of cmpConcurrentFunction is resumed, calling then() method on the promise returned by Promise.resolve(). The value of the inprogressPromise is updated by assigning it a new promise returned by inprogressPromise.then(...)

  7. Function paused as a result of second invocation of cmpConcurrentFunction is resumed. From step 6, we know that inprogressPromise is now a promise returned by inprogressPromise.then(...). So, calling then() on it, we are simply creating a promise chain, adding a new then() method call at the end of the promise chain created in step 6.

    At this point, we have a chain that looks like this:

    inProgressPromise
      .then(() => fn('I am called 1 second later'))
      .then(() => fn('I am called 2 seconds later')) 
    
  8. Callback function of the first then method is called, which in turn calls the fn argument, which is initCmp function. Calling initCmp sets up a timer that resolves the promise initCmp returns after 1 second. When the promise is resolved after 1 second, 'I am called 1 second later' is logged on the console.

  9. When the promise returned by the first call to initComp function in the callback function of first then method, is resolved, it resolves the promise returned by the first then method. This leads to the invocation of the callback function of the second then method. This again calls the initComp function, which then returns a new promise that is resolved after another 1 second.

这就是为什么在 2 秒后,控制台会记录'I am called 2 seconds later' 的原因。
现在回答你的问题:

但如果我删除await inprogressPromise,为什么这段代码仍然可以工作呢?

await inprogressPromise 在你的代码中没有任何用处,除了暂停对 cmpConcurrentFunction 函数的调用。两个调用都等待同一个由 Promise.resolve() 返回的 promise。

所以这个 await 是不必要的?为什么?

因为你在控制台上看到的输出结果并不是因为使用了await,而是因为通过两次调用 cmpConcurrentFunction 函数构建了一个 promise 链(步骤7)。

Further more i was expecting this to work :

const disallowConcurrency = (fn) => {   
  let inprogressPromise = Promise.resolve();

  return async (...args) => {
    await inprogressPromise;
    inprogressPromise = fn(...args);   
  }; 
};
由于现在没有构建Promise链,这段代码与原始代码不同,而Promise链是原始代码产生输出的关键。
使用此实现的disallowConcurrency,您的代码将按以下方式执行:
1. 调用cmpConcurrentFunction函数,并将“I am called 1 second later”作为参数传递。 2. 等待inprogressPromise暂停函数执行。 3. 第二次调用cmpConcurrentFunction,并将“I am called 2 seconds later”作为参数传递。 4. 等待inprogressPromise暂停函数执行。此时,两次调用cmpConcurrentFunction都已暂停,并且两个函数调用都等待由调用Promise.resolve()产生的相同promise。 这是关键点:cmpConcurrentFunction函数的两个调用都在等待由Promise.resolve()创建的相同promise。为什么两个调用都在等待相同的promise?因为在第一次调用cmpConcurrentFunction恢复并调用initComp函数之前,第二次调用cmpConcurrentFunction就被调用了。 5. 恢复第一个调用cmpConcurrentFunction,调用initComp函数,并将其返回值分配给inprogressPromise。调用initComp函数设置一个计时器,在1秒后解决initComp函数返回的promise。 6. 恢复第二个调用cmpConcurrentFunction,调用initComp函数,并将其返回值分配给inprogressPromise,覆盖当前inprogressPromise的值,即第一次调用initComp函数返回的promise。调用initComp函数设置一个计时器,在1秒后解决initComp函数返回的promise。 7. 此时,initComp函数已被调用两次,设置了两个单独的计时器,每个计时器在1秒后解决其各自的promise。
综上所述,disallowConcurrency函数的原始实现与此实现之间的差异在于,在原始实现中,您拥有按顺序解析承诺的promise链,而在第二个实现中,您有两个不相互依赖的单独的promise,并且它们分别在1秒后解决。

1
如果您尝试执行所示代码,您会发现第二个console.log将在2秒后执行,因为它将等待第一个Promise被解析。我的意思是函数的执行程序会立即调用,但是在解析之前会等待第一个Promise被解析。 - Miriam
1
首先感谢您抽出时间提供如此详细的答案,非常感激。 我有一个问题。在最后一段代码中,我正在等待承诺(我认为它应该暂停下一行的执行,直到承诺被解决),然后我将新的承诺返回给inprogressPromise。通过这个,我期望解决第一个承诺,然后调用第二个承诺。但我想我错过了什么。 - Miriam
1
我对“await”版本为什么不起作用的解释也不太信服。显然它是在两个独立的链中,但由于“inprogressPromise”是共享的,我可以理解你期望一个函数首先执行“inprogressPromise = fn(...args);”,导致另一个函数命中“await inprogressPromise”并阻塞,直到第一个完成,就像“then”一样,最后才能执行“fn”。 - ggorlen
我已经添加了详细的解释,为什么disallowConcurrency的第二个实现不能按预期工作。 - Yousaf
1
现在更清楚了,再次感谢您花时间进行详细的解释。因此,在这两种情况下,开头的两个调用都在等待相同的Promise.resolve(),但不同之处在于通过同步链接一个promise的返回值到另一个的then中将会一个接一个地执行它们。 - Miriam
没错!在原始代码中,您创建了一个按顺序执行的Promise链,而在第二个代码示例中,您没有Promise链;相反,您只有两个彼此不依赖且都在1秒后解决的单独的Promise。 - Yousaf

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