【问题标题】:Using async await in recursive function with process.nextTick()在带有 process.nextTick() 的递归函数中使用异步等待
【发布时间】:2019-04-18 05:50:00
【问题描述】:

我在 node.js 应用程序中有一个有效的递归函数,它使用带有 process.nextTick() 回调的 Promises。我很好奇这将/可能如何与异步等待一起工作。

我尝试了一些不同的方法,但无论我做了什么,异步函数都会在所有 nextTick 回调完成之前返回到调用函数。

不工作(删除从快速路由调用的缓存)

const deleteCache = async () => {
  try {
    const cacheRef = fsDb.collection('Cache');
    return await deleteDocsBatch(cacheRef, 30);
  } catch (e) {
    console.error('error in deleteCache:' + e);
  }
};

const deleteDocsBatch = async (cacheRef, batchSize) => {
  try {
    // get all the cached docs, limit to 30 to avoid potential memory issues
    const snapShot = await cacheRef.limit(batchSize).get();
    if (snapShot.size === 0) { return; }

    const batch = fsDb.batch();
    snapShot.docs.forEach((doc) => {
      batch.delete(doc.ref);
    });

    await batch.commit();
    process.nextTick(() => {
      deleteDocsBatch(cacheRef, batchSize);
    });
  } catch (e) {
    console.error('error in deleteDocsBatch:' + e);
  }
};

工作:

function deleteCollection (batchSize) {
  var collectionRef = fsDb.collection('Cache');
  var query = collectionRef.orderBy('__name__').limit(batchSize);

  return new Promise((resolve, reject) => {
    deleteQueryBatch(fsDb, query, batchSize, resolve, reject);
  });
}

function deleteQueryBatch (db, query, batchSize, resolve, reject) {
  query.get()
    .then((snapshot) => {
      // When there are no documents left, we are done
      if (snapshot.size === 0) {
        return new Promise((resolve, reject) => { resolve(0); });
      }

      // Delete documents in a batch
      var batch = db.batch();
      snapshot.docs.forEach((doc) => {
        batch.delete(doc.ref);
      });

      return new Promise((resolve, reject) => {
        batch.commit().then(() => {
          resolve(snapshot.size);
        })
          .catch(reject);
      });
    }).then((numDeleted) => {
      if (numDeleted === 0) {
        resolve();
        return;
      }

      // Recurse on the next process tick, to avoid
      // exploding the stack.
      process.nextTick(() => {
        deleteQueryBatch(db, query, batchSize, resolve, reject);
      });
    })
    .catch(reject);
}

是否可以使用 async await 使用 nexttick() 编写此递归函数?

原始 Firestore 代码示例:

https://firebase.google.com/docs/firestore/manage-data/delete-data

【问题讨论】:

    标签: node.js async-await lifecycle event-loop


    【解决方案1】:

    好的,所以利用这样一个事实,当您通过.then() 将任何内容附加到 Promise 时,它​​将在下一个滴答中运行。换句话说,您的 process.nextTick 在您的原始代码中甚至都不是必需的。在最坏的情况下,您将进入递归调用但立即退出。永远不要超过深度 1。

    await.then() 的语法糖。在引擎盖下,代码无论如何都会转换为.then() 系列。所以这个

    await deleteDocsBatch(cacheRef, batchSize);
    

    而不是

    process.nextTick(() => {
      deleteDocsBatch(cacheRef, batchSize);
    });
    

    应该足够了。正如我所提到的,函数的初始同步部分(即直到第一次等待)将递归运行。因此,如果您想真正确定,则可以通过

    强制异步
    await Promise.resolve();
    await deleteDocsBatch(cacheRef, batchSize);
    

    关键是你告诉解释器“嘿,这是一个异步点,去做点别的吧,就像什么都没有,好吗?”。

    另请注意,await new Promise(res => process.nextTick(res)); 是一种替代方法。虽然有点矫枉过正。


    一个例子:

    async function p1() {
        console.log('interrupt');
    };
    
    async function p2() {
        console.log('1');
        await Promise.resolve();
        console.log('2');
    };
    
    p2();
    p1();
    

    如您所见,这两个函数实际上是同步的。除了p2 不是因为它里面有await。而await 将下面的所有内容强制到下一个刻度,允许p1 在两者之间运行。 p2 函数等价于

    function p2() {
        console.log('1');
        return Promise.resolve().then(() => {
            console.log('2');
        });
    };
    

    输出是:

    1
    interrupt
    2
    

    还有另一个例子。这非常快地超过了最大递归深度:

    async function go(i)
    {
      console.log(i);
      go(i+1);
    }
    
    go(0);
    

    这不是。永远。

    async function go(i)
    {
      console.log(i);
      await Promise.resolve();
      go(i+1);
    }
    
    go(0);
    

    第二个代码实际上使用了一个恒定的内存量。


    结论:函数内的任何(可达)await 都会中断递归调用。这就是代码的样子:

    const deleteDocsBatch = async (cacheRef, batchSize) => {
      try {
        // get all the cached docs, limit to 30 to avoid potential memory issues
        const snapShot = await cacheRef.limit(batchSize).get();
        if (snapShot.size === 0) { return; }
    
        const batch = fsDb.batch();
        snapShot.docs.forEach((doc) => {
          batch.delete(doc.ref);
        });
    
        await batch.commit();
        await deleteDocsBatch(cacheRef, batchSize);
      } catch (e) {
        console.error('error in deleteDocsBatch:' + e);
      }
    };
    

    您应该担心的唯一递归问题是停止条件(例如:是否存在该函数永远不会结束的情况?)。该代码不会吃掉你的记忆。

    【讨论】:

    • 我不是被否决了。我尝试了 await Promise.resolve();和“矫枉过正”,都没有达到预期的效果。在这两种情况下,一些 nextick 都是在父函数返回给调用者之后运行的。
    • @RichWilliams 我在最后添加了完整的代码。你是说这段代码在退出后会在后台产生一些执行吗?听起来不太可能。除非batch.delete() 是异步的? (我不知道那是什么库)
    猜你喜欢
    • 2020-09-08
    • 2021-08-28
    • 2022-01-27
    • 2015-08-27
    • 2016-02-21
    • 1970-01-01
    • 2021-05-12
    • 1970-01-01
    • 2020-11-11
    相关资源
    最近更新 更多