【问题标题】:Promise executes then function before previous then execution is completedPromise 在之前的 then 执行完成之前执行 then 函数
【发布时间】:2019-02-22 14:49:42
【问题描述】:

我尝试链接几个顺序执行的 then 函数,但最后一个 .then() 是在前一个完成执行之前执行的,因此它发送一个空的有效负载。以下是sn-p:

router.get("/selectedHotels", function(req, res) {
  let payload = [];
  return collectionRef
    .where("isOwner", "==", true)
    .get() //fetches owners
    .then(snapshot => {
      snapshot.forEach(user => {
        console.log("User", user);
        collectionRef
          .doc(user.id)
          .collection("venues")
          .get() // fetches hotels from owners
          .then(snapshot => {
            snapshot.forEach(doc => {
              if (
                doc.data().location.long == req.query.long &&
                doc.data().location.lat == req.query.lat
              ) {
                console.log(doc.id, "=>", doc.data());
                payload.push({
                  id: doc.id,
                  data: doc.data()
                });
              }
            });
          })
          .catch(err => {
            console.log("No hotels of this user", err);
          });
      });
    })
    .then(() => {
      console.log("Payload", payload);
      response(res, 200, "Okay", payload, "Selected hotels");
    })
    .catch(err => {
      console.log("Error getting documents", err);
      response(res, 404, "Data not found", null, "No data available");
    });
});

有什么建议吗?谢谢

【问题讨论】:

  • 第一个 then 的内部没有返回 Promise,您正在迭代快照并且什么也不返回。
  • 在这种情况下为什么需要两个外部 then()?
  • @nem035:为什么要编辑?它的可读性很强。
  • @ScottSauyet 我 prettified 代码
  • @nem035:我认为在开始时代码的格式合理时不合适。原始格式也不符合我的喜好,但这不是重点。

标签: javascript node.js firebase promise es6-promise


【解决方案1】:

您的主要错误是在嵌套的 Promise 链的中间有一个非 Promise 返回函数 forEach

router.get('/selectedHotels',function(req,res){ 
  let payload = [];
  return collectionRef.where(...).get()
    .then((snapshot)=>{
        snapshot.forEach(user => {
//      ^^^^^^^^^^^^^^^^^ this means the outer promise doesn't wait for this iteration to finish
// ...

最简单的解决方法是映射您的承诺数组,将它们传递到 Promise.all 并返回它们:

router.get('/selectedHotels',function(req,res){ 
  let payload = [];
  return collectionRef.where(...).get()
    .then((snapshot)=> {
      return Promise.all(snapshot.map(
        // ...
        return collectionRef.doc(user.id).collection('venues').get()
          .then(...)
      ))

话虽如此,像这样嵌套 promise 是一种反模式。 Promise 链允许我们通过 then 回调传播值,因此无需嵌套它们。

相反,您应该将它们垂直连接起来。

下面是一个示例,说明如何做到这一点:

router.get("/selectedHotels", function(req, res) {
  return collectionRef
    .where("isOwner", "==", true)
    .get() //fetches owners
    // portion of the chain that fetches hotels from owners
    // and propagates it further
    .then(snapshot =>
      Promise.all(
        snapshot.map(user =>
          collectionRef
            .doc(user.id)
            .collection("venues")
            .get()
        )
      )
    )
    // this portion of the chain has the hotels
    // it filters them by the req query params
    // then propagates the payload array
    // (no need for global array)
    .then(snapshot =>
      snapshot
        .filter(
          doc =>
            doc.data().location.long == req.query.long &&
            doc.data().location.lat == req.query.lat
        )
        .map(doc => ({ id: doc.id, data: doc.data() }))
    )
    // this part of the chain has the same payload as you intended
    .then(payload => {
      console.log("Payload", payload);
      response(res, 200, "Okay", payload, "Selected hotels");
    })
    .catch(err => {
      console.log("Error getting documents", err);
      response(res, 404, "Data not found", null, "No data available");
    });
});

【讨论】:

  • 由于某种原因,地图功能不起作用,Error getting documents TypeError: snapshot.map is not a function at collectionRef.where.get.then.snapshot (/srv/routes/api/venue/index.js:24:22) at <anonymous> at process._tickDomainCallback (internal/process/next_tick.js:228:7)
  • @nem035,我看到了两个错误:.then(snapshot => snapshot.filter( 这里的参数应该是一个矩阵:doc[][] 你必须先把它弄平.filter().map()。你摆脱了.catch(err => { console.log("No hotels of this user", err); }); 这意味着如果单个用户没有酒店,你的承诺就会失败;我还会通过return [] 扩展此捕获以与我提到的扁平化正确集成
【解决方案2】:

您使用 Firestore,因此您需要将所有文档提供给映射,并且您还需要将一些值返回到下一个。我希望这能帮助你解决你的问题。

router.get('/selectedVenues',function(req,res){
    return collectionRef.where('isOwner', '==', true).get() 
    .then(snapshot => {
        let venues = [];
        snapshot.docs.map(user => {
            venues.push(collectionRef.doc(user.id).collection('venues').get());
        });
        return Promise.all(venues);
    }).then(snapshots => {
        let payload = [];
        snapshots.forEach(venues => {
            venues.docs
                .filter(doc => 
                    doc.data().longitude == req.query.lng && 
                    doc.data().latitude == req.query.lat
                )
                .map(doc => 
                    payload.push({
                        id: doc.id,
                        data: doc.data()
                    })
                ) 
        });
        return payload ;
    }).then(payload => {
        console.log('Payload', payload);
        response(res, 200, "Okay", payload, "Selected hotels");
    }).catch(err => {
        console.log('Error getting documents', err);        
        response(res, 404, 'Data not found', null, 'No data available');
    });
});

【讨论】:

    【解决方案3】:

    您没有从第一个 then 中返回承诺,因此代码无法知道它应该等待异步结果。

    router.get('/selectedHotels',function(req,res){ 
        let payload = [];
        return collectionRef.where('isOwner', '==', true).get() //fetches owners
        .then((snapshot)=>{
            var userVenuesPromises = [];
            snapshot.forEach(user => {
                userVenuesPromises.push(collectionRef.doc(user.id).collection('venues').get());
    
            })
            return Promise.all(userVenuesPromises);
        })
        .then((snapshots) => {
            snapshots.forEach((snapshot) => {
                snapshot.forEach((doc)=> {
                    if (doc.data().location.long == req.query.long && doc.data().location.lat == req.query.lat){
                        console.log(doc.id, '=>', doc.data());
                        payload.push({
                            id: doc.id,
                            data: doc.data()
                        });
                    }
                });
            });
            return payload;
        })
        .then((payload) => {
            ...
    

    除了使用Promise.all() 确保在继续下一步之前完成所有嵌套加载之外,这还删除了嵌套承诺,而是在一个额外的步骤中从快照中解压缩值。

    【讨论】:

      【解决方案4】:

      当使用异步工作链接 .then 时,您需要在执行下一个 .then 之前返回要解决的承诺。像这样:

          return Promise.all(snapshot.map(user => {
                  console.log("User", user);
                  return collectionRef.doc(user.id).collection('venues').get() // fetches hotels from owners
                  .then(snapshot => {
                      snapshot.forEach((doc)=> {
                              if (doc.data().location.long == req.query.long && doc.data().location.lat == req.query.lat){
                                  console.log(doc.id, '=>', doc.data());
                                  payload.push({
                                      id: doc.id,
                                      data: doc.data()
                                  });
                              }
                          });
                      }).catch((err)=>{
                          console.log('No hotels of this user', err);        
                      });        
              });
          )
      

      您可以在这个示例 sn-p 中看到它的实际效果:

      function asyncStuff() {
        return new Promise(resolve => {
          setTimeout(() => {
            console.log('async')
            resolve();
          }, 100)
        });
      }
      
      function doStuff() {
        console.log('started');
        asyncStuff()
        .then(() => {
          return Promise.all([0,1,2].map(() => asyncStuff()));
        })
        .then(() => {
          console.log('second then');
        })
        .then(() => console.log('finished'));
      }
      
      doStuff();

      并且看到没有返回它会给出你的初始行为:

      function asyncStuff() {
        return new Promise(resolve => {
          setTimeout(() => {
            console.log('async')
            resolve();
          }, 100)
        });
      }
      
      function doStuff() {
        console.log('started');
        asyncStuff()
        .then(() => {
          Promise.all([0,1,2].map(() => asyncStuff()));
        })
        .then(() => {
          console.log('second then');
        })
        .then(() => console.log('finished'));
      }
      
      doStuff();

      【讨论】:

      • 我们不能用 async-await 代替 setTimeout 吗?
      • @MuhammadZaidIkhlas setTimeout 只是为了模拟异步工作。它只是像您在示例中那样伪造远程请求。 async/await 是一种处理异步工作的方法,它可以(并且希望会)取代 Promise
      • @Logar async/await 不会取代承诺。它增强了它们。 async/await 唯一的变化是它允许我们以同步的方式编写带有 Promise 的代码,并使用像 try/catch 这样的结构。它仍然完全基于承诺。事实上,每个async 函数总是返回一个promise。
      • @nem035 我知道它现在正在巧妙地使用 Promise 和生成器(因为它是 es2017 规范的一部分,怎么可能有所不同),但我认为一旦我们能够使用它在 vanilla 中,它将在本地实现,因此摆脱了对 Promise 的使用。我这里有什么问题吗?
      • 当我说它将取代 Promise 时,这是因为我想不出一个 Promise 比 async/await 更方便的用例,但我很想提高我对此的了解如果您想详细说明主题
      猜你喜欢
      • 2018-04-05
      • 2019-03-07
      • 2019-10-08
      • 2016-05-13
      • 1970-01-01
      • 2019-08-01
      • 2019-09-28
      • 2020-12-03
      • 2021-05-15
      相关资源
      最近更新 更多