异步和顺序触发Vuex动作-我不了解什么?

时间:2019-07-03 23:07:24

标签: javascript firebase vue.js firebase-realtime-database vuex

我有一个Vuex存储,我正在尝试从Firebase实时数据库中获取数据。我最初是在获取用户信息,但是之后,我想获取一些其他信息,这些信息取决于所获取的初始数据。

从代码中可以看到,我正在尝试使用async / await进行此操作,但是,每当触发created()钩子中的两个操作时,用户信息都不会初始化,因此第二个操作失败。

我的用户存储区

    async fetchCreds({ commit }) {
        try {
            firebase.auth().onAuthStateChanged(async function(user) {
                const { uid } = user
                const userDoc = await users.doc(uid).get()

                return commit('SET_USER', userDoc.data())
            })
        } catch (error) {
            console.log(error)
            commit('SET_USER', {})
        }
    }

我的俱乐部动作取决于上述调用

    async fetchClubInformation({ commit, rootState }) {
        try {
            const clubIDForLoggedInUser = rootState.user.clubId
            const clubDoc = await clubs.doc(clubIDForLoggedInUser).get()

            return commit('SET_CLUB_INFO', clubDoc.data())
        } catch (error) {
            console.log(error)
        }
    }
}

在组件的created()方法中调用的方法。

  created: async function() {
        await this.fetchCreds();
        await this.fetchClubInformation();
        this.loading = false;
  }

我觉得我从根本上误解了异步/等待,但我不明白代码中有什么不正确-任何帮助或建议都将不胜感激。

2 个答案:

答案 0 :(得分:1)

简而言之

fetchCreds({ commit }) {
    return new Promise((resolve, reject) => {
    try {
        firebase.auth().onAuthStateChanged(async function(user) {
            const { uid } = user
            const userDoc = await users.doc(uid).get()

            commit('SET_USER', userDoc.data())
            resolve()
        })
    } catch (error) {
        console.log(error)
        commit('SET_USER', {})
        resolve()
    }}
}

async () => undefined // returns Promise<undefined> -> undefined resolves immediatly

asnyc () => func(cb) // returns Promise<any> resolves before callback got called

() => new Promise(resolve => func(() => resolve())) // resolves after callback got called 

答案 1 :(得分:1)

我对Firebase并不是特别熟悉,但是在对源代码进行了一些挖掘之后,我认为我可以对您的问题有所了解。

首先,请考虑以下示例:

async function myFn (obj) {
  obj.method(function () {
    console.log('here 1')
  })

  console.log('here 2')
}

await myFn(x)
console.log('here 3')

问题:您将以什么顺序查看日志消息?

here 2肯定会在here 3之前出现,但是从上面的代码中无法判断here 1何时会出现。这取决于obj.method对传递的功能的处理方式。它可能根本不会调用它。它可能会同步调用它(例如Array的forEach方法),在这种情况下,here 1会出现在其他消息之前。如果它是异步的(例如计时器,服务器调用),那么here 1可能会在here 3之后很长一段时间内不显示。

如果async修饰符本身未返回Promise,则将隐式地从该函数返回Promise。该Promise的解析值将是该函数返回的值,并且 Promise将在该函数返回的点解析。对于最后没有return的函数,等同于以return undefined结尾的函数。

因此,为了强调关键点,异步函数返回的Promise将只等到该函数返回。

方法onAuthStateChanged异步调用其回调,因此该回调中的代码要等到周围函数完成后才能运行。没有什么可以告诉隐式返回的Promise等待该回调被调用的。回调中的await无关紧要,因为该函数尚未被调用。

Firebase广泛使用Promise,因此通常的解决方案是returnawait相关的Promise:

// Note: This WON'T work, explanation follows
return firebase.auth().onAuthStateChanged(async function(user) {
// Note: This WON'T work, explanation follows
await firebase.auth().onAuthStateChanged(async function(user) {

这在这里不起作用,因为onAuthStateChanged实际上没有返回Promise,而是返回了unsubscribe函数。

您当然可以自己创建一个新的Promise并以这种方式“修复”。但是,使用new Promise创建新的Promises通常被认为是代码气味。通常,仅在包装不正确支持Promises的代码时才有必要。如果我们使用的是具有适当Promise支持的库(如此处所示),则无需创建任何Promises。

那么onAuthStateChanged为什么不返回承诺?

因为这是观看所有登录/注销事件的一种方式。每次用户登录或注销时,都会调用回调。它并不是用来观看特定登录的方式。一个Promise只能被解析为一个值。因此,尽管可以使用Promise对单个登录事件进行建模,但在观看所有登录/退出事件时却毫无意义。

因此fetchCreds正在注册,以通知所有登录/注销事件。它对返回的unsubscribe函数不做任何事情,因此大概它将监听所有此类事件,直到重新加载页面为止。如果您多次拨打fetchCreds,它将继续添加越来越多的侦听器。

如果您正在等待用户完成登录,那么建议您直接等待。 firebase.auth()有多种以前缀signIn开头的方法,例如signInWithEmailAndPassword,并且它们确实会返回一个Promise,该Promise在用户完成登录后进行解析。解析后的值提供对包括用户在内的各种信息的访问。我不知道您使用的是哪种方法,但是所有方法的想法都差不多。

但是,您可能真的只是对获取当前用户的详细信息感兴趣。如果仅此而已,则完全不需要使用onAuthStateChanged。您应该只能够使用currentUser属性来获取副本。像这样:

async fetchCreds({ commit }) {
   try {
      const { uid } = firebase.auth().currentUser
      const userDoc = await users.doc(uid).get()

      commit('SET_USER', userDoc.data())
   } catch (error) {
      console.log(error)
      commit('SET_USER', {})
   }
}

正如我已经提到的,这基于用户已经登录的假设。如果这不是一个安全的假设,那么您可能要考虑等到登录完成后再创建需要用户的组件凭据。

更新

评论中的问题:

  

如果obj.method()调用是异步的,并且我们确实在其中等待回调函数,那是否可以确保外部异步函数(myFn)在内部函数完成之前从未解析?

我不确定您在这里问什么。

请清楚一点,我对async asynchronous 这两个词的使用非常小心。诸如setTimeout之类的函数将被视为异步,但不是async

async / await只是Promises周围的许多语法糖。您并不是真正在等待功能,而是在等待Promise。当我们谈论等待async函数时,我们实际上是在等待等待Promise返回解决。

因此,当您说等待回调函数时,并不清楚这意味着什么。您要尝试await哪个承诺?

async修饰符放在函数上并不能使其神奇地等待事件。它只会在遇到await时等待。您仍可以在async函数中进行其他异步调用,并且与普通函数一样,这些调用将在函数返回后执行。 “暂停”的唯一方法是await一个承诺。

await放入另一个函数(甚至是嵌套函数)中,除非外部函数已经在等待内部函数,否则对外部函数是否等待没有任何影响。在幕后,这仅仅是Promises链接的then调用。每当您写await时,您都只是向Promise添加了另一个then调用。但是,除非Promise与外部async函数返回的Promise处于同一链中,否则这将不会达到预期的效果。只需丢失一个链接即可使链失效。

因此,修改我之前的示例:

async function myFn (obj) {
  await obj.method(async function () {
    await somePromise

    // ...
  })

  // ...
}

await myFn(x)

请注意,这里有3个函数:myFnmethod和回调传递给method。问题是,await myFn(x)是否会等待somePromise

根据上面的代码,我们实际上无法分辨。这将取决于method内部所做的事情。例如,如果method看起来像这样,它将仍然无法工作:

function method (callback) {
  setTimeout(callback, 1000)
}

async放在method上无济于事,这只会使其返回一个Promise,但Promise仍然不会等待计时器触发。

我们的Promise链链接断开。 myFn和回调函数都在创建链的各个部分,但是除非method将这些Promises链接在一起,否则它将不起作用。

另一方面,如果编写method来返回一个合适的Promise,它等待回调完成,那么我们将获得目标行为:

function method (callback) {
  return someServerCallThatReturnsAPromise().then(callback)
}

我们可以在这里使用async / await,但是没有必要,因为我们可以直接返回Promise。

  

此外,如果在异步myFn函数中您没有返回任何内容,是否意味着它将立即解析为未定义内容?

立即一词在这里定义不明确。

  1. 如果函数最后没有返回任何内容,则等同于结尾有return undefined
  2. async函数返回的Promise将在函数返回时解决。
  3. 已解决的Promise值将是返回的值。

因此,如果您不返回任何内容,它将解析为undefined。直到功能结束,解析才会发生。如果该函数不包含任何await调用,则将与立即返回“立即”的同步函数一样,“立即”发生。

但是,await只是then调用周围的语法糖,而then调用始终是异步的。因此,尽管Promise可能“立即”解决,但await仍然需要等待。等待时间很短,但不是同步的,其他代码可能有机会同时运行。

请考虑以下内容:

const myFn = async function () {
  console.log('here 3')
}

console.log('here 1')

Promise.resolve('hi').then(() => {
  console.log('here 4')
})

console.log('here 2')

await myFn()

console.log('here 5')

日志消息将按编号顺序显示。因此,即使myFn立即解决,您仍然可以在here 4here 3之间跳入here 5