【问题标题】:Why does my async function always return undefined?为什么我的异步函数总是返回未定义?
【发布时间】:2021-08-01 04:28:30
【问题描述】:

似乎我使用异步错误,有人能发现我做错了什么吗?

这是我正在等待的功能:

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
  var tradeInstanceID;
  var senderID;
  var receiverID;
  var senderItemsTemp;
  var receiverItemsTemp;
  var response;
  var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
  return tradeOffer
    .get()
    .then((doc) => {
      senderItemsTemp = doc.data().sendersItems;
      receiverItemsTemp = doc.data().receiversItems;
      senderID = doc.data().senderID;
      receiverID = doc.data().receiverID;
    })
    .then(() => {
      var itemInTrade = false;
      senderItemsTemp.forEach((item) => {
        db.collection("listings")
          .doc(item.itemID)
          .get()
          .then((doc) => {
            if (doc.data().status !== "listed") {
              itemInTrade = true;
            }
          })
          .then(() => {
            receiverItemsTemp.forEach((item) => {
              db.collection("listings")
                .doc(item.itemID)
                .get()
                .then((doc) => {
                  if (doc.data().status !== "listed") {
                    itemInTrade = true;
                  }
                })
                .then(() => {
                  if (itemInTrade) {
                    tradeOffer.update({
                      status: "declined",
                    });
                    return false;
                  } else {
                    db.collection("trades")
                      .add({
                        tradeOfferID: tradeOfferID,
                        senderTradeStatus: {
                          created: true,
                          sentToSeekio: "current",
                          inspection: false,
                          sentToPartner: false,
                        },
                        receiverTradeStatus: {
                          created: true,
                          sentToSeekio: "current",
                          inspection: false,
                          sentToPartner: false,
                        },
                        postagePhotos: [],
                        inspectionPhotos: [],
                        senderPaid: false,
                        receiverPaid: false,
                        senderUploadedProof: false,
                        receiverUploadedProof: false,
                        senderID: senderID,
                        receiverID: receiverID,
                        messages: [
                          {
                            message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
                            sender: "System",
                            timestamp: firebase.firestore.Timestamp.fromDate(
                              new Date()
                            ),
                          },
                        ],
                      })
                      .then((docRef) => {
                        tradeInstanceID = docRef.id;
                        tradeOffer
                          .set(
                            {
                              status: "accepted",
                              tradeInstanceID: docRef.id,
                            },
                            { merge: true }
                          )
                          .then(() => {
                            var receiver = db.collection("users").doc(senderID);
                            var notification = {
                              from: auth.currentUser.uid,
                              fromUsername: userData.username,
                              type: "tradeOfferAccepted",
                              time: firebase.firestore.Timestamp.fromDate(
                                new Date()
                              ),
                              seen: false,
                            };
                            receiver
                              .update({
                                notifications: firebase.firestore.FieldValue.arrayUnion(
                                  notification
                                ),
                              })
                              .then(() => {
                                response = {
                                  sendersItems: senderItemsTemp,
                                  receiversItems: receiverItemsTemp,
                                };
                                return response;
                              });
                          });
                      })
                      .catch((err) => console.log(err));
                  }
                });
            });
          });
      });
    });
}

这就是我所说的:

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  async function acceptTradeOffer() {
    var tradeOfferID = currentTradeFocus;
    var senderID = "";
    setLoading("loading");
    if (userData !== null && tradeOfferID !== "") {
      const response = await firebaseAcceptTradeOffer(
        currentTradeFocus,
        userData
      );
      console.log(
        "RESPONSE FROM FIREBASE SERVICE>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>: ",
        response
      );
      if (!response) {
        setErrorMsg("One of the selected items is no longer available.");
      } else if (
        response.sendersItems !== null &&
        response.receiversItems !== null
      ) {
        setSenderItems(response.sendersItems);
        setReceiverItems(response.receiversItems);
        toggleConfirmScreen("cancel");
        setLoading("idle");
        setItemsSet(true);
      }
      fetch(
        "https://europe-west2-seekio-86408.cloudfunctions.net/sendMail?type=tradeUpdate&userID=" +
          senderID
      ).catch((err) => {
        console.log(err);
        setLoading("idle");
      });
    }
  }

所以基本上我想去检查一下这个“交易”中的任何项目是否不等于“列出”(这意味着它们不可用,我想返回false,如果没有,那么我返回物品数组,以便交易可以继续。

编辑:我试图重新调整它,但它只工作了一半。从顶层看我正在尝试做的事情:

User wants to accept a trade offer for some items >
Check through all items to make sure they are available and not sold >
If so, accept the trade >
Then once its accepted, go and cancel all remaining trade offers that include items from this accepted trade, cause they are not available anymore.


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
  console.log(
    "----- starting firebaseAcceptTradeOffer--------- ",
    unavailableItem
  );
  //==============
  var tradeInstanceID;
  var senderID;
  var receiverID;
  var senderItemsTemp;
  var receiverItemsTemp;
  var unavailableItem = false;
  var response;
  var itemsArray;
  var notListed = false;
  //==============
  var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);

  unavailableItem = tradeOffer
    .get()
    .then((doc) => {
      senderID = doc.data().senderID;
      receiverID = doc.data().receiverID;
      itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
    })
    .then(() => {
      itemsArray.forEach((item) => {
        db.collection("listings")
          .doc(item.itemID)
          .get()
          .then((doc) => {
            if (doc.data().status !== "listed") {
              notListed = true;
            }
          });
      });
    })
    .then(() => {
      return notListed;
    });

  console.log(
    "-----unavailableItem at the end of method --------- ",
    unavailableItem
  );

  //^^^^^^^^^^ here i am getting a promise result of false (which is correct) but HOW CAN I ACCESS IT

  if (unavailableItem) {
    tradeOffer.update({
      status: "declined",
    });
    return false;
  } else {
    response = await createTrade(
      tradeOffer,
      tradeOfferID,
      senderID,
      receiverID,
      userData.username
    );
    console.log("response from createTrade", response);
    return response;
  }
}

我得到了一个带有上述值false 的承诺对象。 False 是我期望的正确值,但我怎样才能访问它?它是 Promise 对象的形式吗?

【问题讨论】:

  • 您将async + await.then() 回调混合在一起。不要那样做。
  • 简而言之,您会得到undefined,因为您在第二个then() 处理程序中没有返回任何值。您需要重构您的代码以使用Promise.all() 并将其适当地挂接到您的承诺链中。重构这种大小的代码超出了 StackOverflow 的范围。如果您在使用Promise.all() 进行破解后仍然卡住,请在下次尝试时编辑您的问题。
  • 当您通过 ID 获取许多文档时,您可以从使用 this helper function 中受益,这将有助于重构您的代码。
  • @samthecodingman 感谢 m,an,打算尝试使用 Promise.all,如果我失败了会添加编辑,谢谢!
  • @samthecodingman 我卡住了哈哈,刚刚在上面做了一个编辑!

标签: javascript reactjs firebase google-cloud-firestore async-await


【解决方案1】:

一般来说,当您返回一个无法解决的承诺时,您必须等待它的结果。此外,您必须从 Promise then 链中返回一个值,至少最后一个 .then() 需要返回一个值,这也可以在 .finally() 方法中完成。

使用从任何 firebase 资源获取,实时、firestore 和存储都是异步进程,必须等待。在您的情况下,您缺少等待返回:

 var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
  return tradeOffer

而且您似乎没有在 .then() 语句中返回任何内容,我建议您完全重写您正在尝试的内容,以便根据需要返回值。

【讨论】:

  • 感谢您的评论,我正在努力从 firebase 的搜索中获得承诺结果,我现在将编辑我的答案
【解决方案2】:

我有一些时间,所以让我们分解一下。

变量说明

如果您不使用 TypeScript(即使您使用),我强烈建议您将类型插入到变量的名称中。

db                # ✔ by convention, either firebase.database() or firebase.firestore()
tradeOffer        # ❓ type unclear, could be a number, an object, a string, etc
tradeOfferDocRef  # ✔ a DocumentReference
trades            # ❓ type unclear, plural implies a collection of some sort
tradesColRef      # ✔ a CollectionReference

你也可能遇到这些:

doc               # ❓ by convention, a DocumentSnapshot, but with unknown data
tradeDoc          # ✔ implies a DocumentSnapshot<TradeData> (DocumentSnapshot containing trade data)

当仅使用 doc 时,您需要查看它用于上下文的位置以了解此 DocumentSnapshot 包含的内容。

db.collection('trades').doc(tradeOfferID).get()
  .then((doc) => { // contents implied to be TradeData
    const data = doc.data();
  });
// or
tradeDocRef.get()
  .then((doc) => { // contents implied to be TradeData
    const data = doc.data();
  });

您应该酌情重命名doc,尤其是在使用async/await 语法时,这样您就不会出现以下情况:

const doc = await db.collection('trades').doc(tradeOfferID).get();
/* ... many lines ... */
const senderID = doc.get("senderID"); // what was doc again?

正如您在问题中标记了 reactjs,这意味着您正在使用现代 JavaScript。

放弃对var 的任何使用,并将其替换为块范围版本:const(防止重新分配变量)或let(类似于varbut not quite)。这些更安全,可以防止意外覆盖不应该覆盖的内容。

您还可以使用对象解构来分配变量。

const senderID = doc.data().senderID;
const receiverID = doc.data().receiverID;
const itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);

可以变成:

const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);

如果您只需要文档中的一个属性,您应该使用DocumentSnapshot#get() 而不是DocumentSnapshot#data(),这样它只会解析您想要的字段而不是整个文档的数据。

function getUserAddress(uid) {
  return firebase.firestore()
    .collection('users')
    .doc(uid)
    .get()
    .then(userDoc => userDoc.get("address")); // skips username, email, phone, etc
}

Promise 说明

var senderID;
var receiverID;
var itemsArray;

tradeOfferDocRef
  .get()
  .then((doc) => {
    senderID = doc.data().senderID;
    receiverID = doc.data().receiverID;
    itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
  })
  .then(() => {
    /* use results from above */
  });

虽然上述代码块按预期运行,但当您有许多这样的变量时,不清楚它们的设置时间和位置。

这也会导致你认为变量有值的问题:

var senderID;
var receiverID;
var itemsArray;

tradeOfferDocRef
  .get()
  .then((doc) => {
    // this line runs after the line below
    senderID = doc.data().senderID;
    receiverID = doc.data().receiverID;
    itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
  });

// this line before the line above
console.log(senderID); // will always log "undefined"

这可以通过以下三种方式之一来避免:

  • 返回数据以传递给下一个处理程序(您不会在本示例中使用它,仅当下一个 then() 处理程序位于其他位置时):
tradeOfferDocRef
  .get()
  .then((doc) => {
    const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
    const itemsArray = sendersItems.concat(receiversItems);
    return { senderID, receiverID, itemsArray }; // pass to next step
  })
  .then((neededData) =>
    /* use neededData.senderID, neededData.receiverID, etc */
  });
  • 在同一处理程序中使用数据:
tradeOfferDocRef
  .get()
  .then((doc) => {
    const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
    const itemsArray = sendersItems.concat(receiversItems);

    /* use results from above */
  });
  • 使用async-await语法:
const tradeDoc = await tradeOfferDocRef.get();

const { senderID, receiverID, sendersItems, receiversItems } = tradeDoc.data();
const itemsArray = sendersItems.concat(receiversItems);

/* use results from above */

写入 Firestore

您当前的代码包含以下步骤:

1. Get the trade offer document</li>
2. If successful, pull out the sender and receiver's IDs, along with any items in the trade
3. If successful, do the following for each item in the sender items array:
  a) Check if any of the sender's items are unavailable</li>
  b) If successful, do the following for each item in the receiver items array:
    - If **any item** was unavailable prior to this, decline the trade & return `false`.
    - If all items **so far** are available, do the following:
      a) Create a document containing information about the trade with the needed data
      b) If successful, edit the trade offer document to accept it
      c) If successful, create a notification for the receiver
      d) If successful, return the traded items
      e) If any of a) to d) fail, log the error and return `undefined` instead
4. Return `undefined`

在上述步骤中,您可以看到您的 Promise 链接存在一些问题。但除此之外,您还可以看到您是一个接一个地创建和编辑文档,而不是一次性(“原子地”)创建和编辑文档。如果这些写入中的任何一个失败,您的数据库最终会处于未知状态。例如,您可以创建并接受交易,但未能创建通知。

要以原子方式写入数据库,您需要使用batched write 将一堆更改捆绑在一起,然后将它们发送到 Firestore。如果其中任何一个失败,则不会更改数据库中的数据。

接下来,您将用户的通知存储在他们的用户文档中。对于少量通知,这很好,但如果您只想提取地址或电话号码,如上一节中的示例,您是否需要下载所有这些通知?我建议将它们分成自己的文档(例如/users/{someUserId}/metadata/notifications),但最好是自己的集合(例如/users/{someUserId}/notifications/{someNotificationID})。通过将它们放在自己的集合中,您可以查询它们并使用QuerySnapshot#docChanges 来同步更改并使用 Cloud Firestore 触发器发送推送通知。

重构函数

1. Get the trade offer document</li>
2. Once the retrieved, do the following depending on the result:
  - If failed or empty, return an error
  - If successful, do the following:
    a) Pull out the sender and receiver's IDs, along with any items in the trade.
    b) For each item in the trade, check if any are unavailable and once the check has completed, do the following depending on the result:
      - If any item is unavailable, do the following:
        a) Decline the trade
        b) Return the list of unavailable items
      - If all items are available, do the following:
        a) Create a new write batch containing:
          - Create a document about the trade
          - Edit the trade offer document to accept it
          - Create a notification for the receiver
        b) Commit the write batch to Firestore
        c) Once the commit has completed, do the following depending on the result:
          - If failed, return an error
          - If successful, return the traded items and the trade's ID

因为这里的步骤相互依赖,所以这是使用async/await 语法的好候选。

要查看实际情况,请仔细研究:

import * as firebase from "firebase-admin";

// insert here: https://gist.github.com/samthecodingman/aea3bc9481bbab0a7fbc72069940e527

async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
  const tradeOfferDocRef = db.collection("tradeOffers").doc(tradeOfferID);

  const tradeDoc = await tradeOfferDocRef.get();

  const { senderID, receiverID, sendersItems, receiversItems } =
    tradeDoc.data();
  const itemsArray = sendersItems.concat(receiversItems);

  // TODO: Check if this is an accurate assumption
  if (sendersItems.length == 0 || receiversItems.length == 0) {
    success: false,
    message: "One-sided trades are not permitted",
    detail: {
      sendersItemsIDs: sendersItems.map(({ itemID }) => itemID),
      receiversItemsIDs: receiversItems.map(({ itemID }) => itemID),
    },
  };

  const listingsColQuery = db
    .collection("listings")
    .where("status", "==", "listed");

  const uniqueItemIds = Array.from(
    itemsArray.reduce(
      (set, { itemID }) => set.add(itemID),
      new Set()
    )
  );

  const foundIds = {};

  await fetchDocumentsWithId(
    listingsColQuery,
    uniqueItemIds,
    (listingDoc) => {
      // if here, listingDoc must exist because we used .where("status") above
      foundIds[listingDoc.id] = true;
    }
  );

  const unavailableItemIDs = uniqueItemIds
    .filter(id => !foundIds[id]);

  if (unavailableItems.length > 0) {
    // one or more items are unavailable!
    await tradeOfferDocRef.update({
      status: "declined",
    });
    return {
      success: false,
      message: "Some items were unavailable",
      detail: {
        unavailableItemIDs,
      },
    };
  }

  const tradeDocRef = db.collection("trades").doc();
  const tradeInstanceID = tradeDocRef.id;

  const batch = db.batch();

  batch.set(tradeDocRef, {
    tradeOfferID,
    senderTradeStatus: {
      created: true,
      sentToSeekio: "current",
      inspection: false,
      sentToPartner: false,
    },
    receiverTradeStatus: {
      created: true,
      sentToSeekio: "current",
      inspection: false,
      sentToPartner: false,
    },
    postagePhotos: [],
    inspectionPhotos: [],
    senderPaid: false,
    receiverPaid: false,
    senderUploadedProof: false,
    receiverUploadedProof: false,
    senderID,
    receiverID,
    messages: [
      {
        message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
        sender: "System",
        timestamp: firebase.firestore.Timestamp.fromDate(new Date()),
      },
    ],
  });

  batch.set(
    tradeOfferDocRef,
    {
      status: "accepted",
      tradeInstanceID,
    },
    { merge: true }
  );

  const receiverNotificationRef = db
    .collection("users")
    .doc(senderID)
    .collection("notifications")
    .doc();

  batch.set(receiverNotificationRef, {
    from: auth.currentUser.uid,
    fromUsername: userData.username,
    type: "tradeOfferAccepted",
    time: firebase.firestore.Timestamp.fromDate(new Date()),
    seen: false,
  });

  await batch.commit();

  return {
    success: true,
    message: "Trade accepted",
    detail: {
      tradeID: tradeInstanceID,
      senderItems,
      receiversItems,
    },
  };
}

用法:

try {
  const tradeResult = await firebaseAcceptTradeOffer(someTradeId);
} catch (err) {
  // if here, one of the following things happened:
  //  - syntax error
  //  - database read/write error
  //  - database rejected batch write
}

【讨论】:

  • 这太棒了。老实说,感谢一群人。我现在要开始阅读这篇文章,看起来是一堂很棒的课,我会评论我的结局,你是个传奇
  • 我刚刚收到一条错误消息,说 uniqueItemIDs.filter() 不是函数。当我在控制台中返回 uniqueItemIDs 时,我可以看到它是一个对象,并且 ids 在 Entries 数组中。有没有办法可以取出这些条目?调用 .entries() 似乎也不起作用。 TIA 伙伴!
  • @squish 啊,我试图变得聪明,却忘记了 Set#values() 返回一个 Iterator 而不是一个数组(Set#entries() 也是如此)。 Set 是这里的新增内容,因为过去我通常为此使用 const dedupeMap = {}arr.forEach(e =&gt; dedupeMap[e.id] = true)const uniqueIds = Object.keys(dedupeMap)。现已修复。
  • 嗨,山姆,您的方法已实施,而且确实很棒。我从这部分代码中学到了很多东西。比如批量和批量提交,如何正确使用异步等待和返回。非常感谢你是一个传奇。
猜你喜欢
  • 2018-03-12
  • 2022-01-16
  • 1970-01-01
  • 2021-09-05
  • 2020-12-25
  • 2021-05-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多