如何确保 Node.js 中的这些函数链按顺序执行(使用 Promise)?

Posted

技术标签:

【中文标题】如何确保 Node.js 中的这些函数链按顺序执行(使用 Promise)?【英文标题】:How can I am make sure these chain of functions in Node.js are performed in order (using promises)? 【发布时间】:2022-01-19 03:06:07 【问题描述】:

我在 Node.js 中有一组函数,我想按特定顺序加载它们。我将提供一些抽象和简化的模型代码:

function updateMyApp() 
loadDataToServer()
.then(() => useData())
.then(() => saveData())
.then(() =>  console.log("updateMyApp done") )


function loadDataToServer() 
return new Promise( (resolve, reject) 
...preparing data and save file to cloud...
resolve())


function handleDataItem(item) 
// Function that fetches data item from database and updates each data item
console.log("Name", item.name)


function saveData() 
// Saves the altered data to some place

useData 有点复杂。在里面我想,按顺序

    console.log('正在启动 alterData()') 从云数据源以 json 格式加载数据 遍历 json 文件中的每一项并对其执行 handleDataItem(item)。 当 #2 完成时 -> console.log('alterData() done') 将已解决的承诺返回给 updateMyApp 继续使用 saveData() 更改所有数据。

我希望显示日志:

Starting useData()
Name: Adam
Name: Ben
Name: Casey
useData() done

我对此的看法如下:

function useData() 
   console.log('Starting useData()')
   return new Promise( function(resolve, reject) 
      readFromCloudFileserver()
      .then(jsonListFromCloud) => 
         jsonListFromCloud.forEach((item) => 
            handleDataItem(item)
         
      )
      .then(() => 
         resolve() // I put resolve here because it is not until everything is finished above that this function is finished
         console.log('useData() done')
      ).catch((error) =>  console.error(error.message) )
   )

这似乎可行,但据我了解,这不是一个人应该这样做的方式。此外,这似乎在此链之外执行handleDataItem,因此日志如下所示:

Starting useData()
useData() done
Name: Adam
Name: Ben
Name: Casey

换句话说。当链移动到下一步 (.then()) 时,handleDataItem() 调用似乎没有完成。换句话说,我不能确定当它继续到saveData() 函数时所有项目都已更新?

如果这不是一个很好的处理方式,那么这些函数应该怎么写呢?如何正确链接函数以确保一切都以正确的顺序完成(以及使日志事件按顺序显示)?

编辑:根据要求,这对 handleDataItem 的抽象程度较低。

function handleDataItem(data) 

   return new Promise( async function (resolve)   
      data['member'] = true 
      if (data['twitter']) 
         const cleanedUsername = twitterApi.cleanUsername(data['twitter']).toLowerCase()       

         if (!data['twitter_numeric']) 
            var twitterId = await twitterApi.getTwitterIdFromUsername(cleanedUsername)
            if (twitterId) 
               data['twitter_numeric'] = twitterId
            
         
         if (data['twitter_numeric'])          
            if (data['twitter_protected'] != undefined) 
               var twitterInfo = await twitterApi.getTwitterGeneralInfoToDb(data['twitter_numeric'])
               data['twitter_description'] = twitterInfo.description
               data['twitter_protected'] = twitterInfo.protected
               data['twitter_profile_pic'] = twitterInfo.profile_image_url.replace("_normal", '_bigger')
               data['twitter_status'] = 2
               console.log("Tweeter: ", data)
             
          else 
            data['twitter_status'] = 1

                  
          

      resolve(data)

   ).then( (data) => 
      db.collection('people').doc(data.marker).set(data)   
      db.collection('people').doc(data.marker).collection('positions').doc(data['report_at']).set(
         
            "lat":data['lat'],
            "lon":data['lon'],
         
      )  
   ).catch( (error) =>  console.log(error) )          

调用的 twitterAPI 函数:

   cleanUsername: function (givenUsername) 
      return givenUsername.split('/').pop().replace('@', '').replace('#', '').split(" ").join("").split("?")[0].trim().toLowerCase()
   ,  

 getTwitterGeneralInfoToDb: async function (twitter_id) 
     var endpointURL = "https://api.twitter.com/2/users/" + twitter_id
     var params = 
       "user.fields": "name,description,profile_image_url,protected"
     

     // this is the HTTP header that adds bearer token authentication
      return new Promise( (resolve,reject) => 
         needle('get', endpointURL, params, 
          headers: 
              "User-Agent": "v2UserLookupJS",
              "authorization": `Bearer $TWITTER_TOKEN`
          
        ).then( (res) => 
           console.log("result.body", res.body); 
           if (res.body['errors']) 
               if (res.body['errors'][0]['title'] == undefined) 
                  reject("Twitter API returns undefined error for :'", cleanUsername, "'")
                else 
                  reject("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
                     
             else  
               resolve(res.body.data)
            
         ).catch( (error) =>  console.error(error.message) )
     )
  ,


  // Get unique id from Twitter user
  // Twitter API
  getTwitterIdFromUsername: async function (cleanUsername) 
    
    const endpointURL = "https://api.twitter.com/2/users/by?usernames="
    const params = 
     usernames: cleanUsername, // Edit usernames to look up     
    


    // this is the HTTP header that adds bearer token authentication
    const res = await needle('get', endpointURL, params, 
     headers: 
         "User-Agent": "v2UserLookupJS",
         "authorization": `Bearer $TWITTER_TOKEN`
     
    )
    if (res.body['errors']) 
      if (res.body['errors'][0]) 
         if (res.body['errors'][0]['title'] == undefined) 
            console.error("Twitter API returns undefined error for :'", cleanUsername, "'")
          else 
            console.error("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
         
       else 
        console.error("Twitter API special error:", res.body)
      
     else 
      if (res.body['data']) 
        return res.body['data'][0].id
       else 
        //console.log("??? Could not return ID, despite no error. See: ", res.body)
      
      

    
  ,

【问题讨论】:

避免Promise constructor antipattern! handleDataItem 是异步的吗?它会返回一个承诺吗? 我们需要查看handleDataItem()的代码。由于它显然是异步的(更新您的数据库)并且在您看到问题的地方,这可能是整个问题的很大一部分。我们需要帮助您修复该代码。 仍在尝试从头到尾遵循代码。你用的是什么数据库? db.collection('people').doc(data.marker).set(data) 是否返回承诺? 我正在使用 Google Cloud Firestore (nosql)。这行代码不返回承诺。我已经开始使用 try/catch 来实施您的解决方案,而且它似乎工作得更好。目前看来,这可能是我的问题的解决方案,但我需要更深入地研究 Promises,因为我真的不太明白。 【参考方案1】:

听起来handleDataItem() 的实现及其返回的承诺存在问题。为了帮助您解决这个问题,我们需要查看该函数的代码。

您还需要清理useData(),以便它正确返回一个传播完成和错误的承诺。

而且,如果 handleDataItem() 返回一个准确的承诺,那么您还需要在此处更改循环执行该操作的方式。

从此改变:

function useData() 
   console.log('Starting useData()')
   return new Promise( function(resolve, reject) 
      readFromCloudFileserver()
      .then(jsonListFromCloud) => 
         jsonListFromCloud.forEach((item) => 
            handleDataItem(item)
         
      )
      .then(() => 
         resolve() // I put resolve here because it is not until everything is finished above that this function is finished
         console.log('useData() done')
      ).catch((error) =>  console.error(error.message) )
   )

到这里:

async function useData() 
    try 
        console.log('Starting useData()')
        const jsonListFromCloud = await readFromCloudFileserver();
        for (let item of jsonListFromCloud) 
            await handleDataItem(item);
        
        console.log('useData() done');
     catch (error) 
        // log error and rethrow so caller gets the error
        console.error(error.message)
        throw error;
    

这里的结构变化是:

    改用async/await 更轻松地处理循环中的异步项 删除将 new Promise() 包裹在现有 Promise 周围的 Promise 反模式 - 不需要这样做,并且您没有捕获或传播来自 readFromCloudFileServer() 的拒绝,这是使用该反模式时的常见错误。李> 在记录错误后在 catch 中重新抛出错误,以便将错误传播回调用者

【讨论】:

非常感谢!关于这个解决方案,我有两个问题。 1. 关于从 readFromCloudFileServer() 中捕获拒绝。我认为这是由拖尾 .catch() 处理的?不是吗? 2. updateMyApp() 不需要“return new Promise”来传播到下一个 .then(),即 saveData()。当我尝试在没有“return new Promise”的情况下执行 useData() 时,它不会继续 saveData()。 @Christoffer - 在您致电 saveData() 之前,您的代码会显示一些 caledl alterData()。您没有显示alterData() 的任何代码,所以我不知道它是什么。与您之前的问题一样,当您只显示代码片段并且我们无法跟踪整个代码执行时,我们很难提供详细、具体的建议。我为您提供了有关 ONE 函数 useData() 的具体建议,您向我展示了完整的代码,并向我展示了它被调用的上下文。 @Christoffer - updateMyApp() 应该以return loadDataToServer() 开头,所以updateMyApp() 的调用者可以获得该承诺链的结果。但是,您没有显示任何调用 updateMyApp() 的代码,所以我不知道这是否有用。我想我已经提供了尽可能多的信息,但没有看到这里的全貌和所有相关代码。很难猜测丢失的部分,坦率地说,尝试推测丢失的代码可能正在做的所有事情只是浪费时间。 我真的很抱歉,因为我真的很感谢你的帮助。我实际上认为如果我抽象出很多代码以使核心问题更加突出会更容易。我现在添加了没有抽象的代码,包括从中调用的其他函数。这样会更容易理解吗?【参考方案2】:

您有 3 个选项来处理循环中的异步方法的主要问题。

    使用map 代替forEach 并返回承诺。然后在返回的 Promise 上使用 Promise.all 等待它们全部完成。

    for/of 循环与 async/await 结合使用。

    使用for await 循环。

【讨论】:

谢谢,我会研究一下并尝试一下。我之前确实尝试过 Promise.all 但从未成功过,但我会再试一次。

以上是关于如何确保 Node.js 中的这些函数链按顺序执行(使用 Promise)?的主要内容,如果未能解决你的问题,请参考以下文章

在Javascript中确保forEach / for循环的正确顺序的简单方法?

如何确保在 Node JS 上的 render() 之前执行查询

如何确保JavaScript的执行顺序

如何确保 onResponse 的执行顺序与请求的顺序相同?

node.js 函数调用顺序,Spotify Web API

确保线程池中的任务执行顺序