我对 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,因此通常解决方案是向 return 或 await 发送相关的 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 创建新的 Promise 通常被认为是代码异味。通常只有在包装不支持 Promises 的代码时才需要。如果我们正在使用具有适当 Promise 支持的库(就像我们在这里一样),那么我们不需要创建任何 Promise。
那么为什么onAuthStateChanged 不返回一个 Promise?
因为这是一种查看所有登录/退出事件的方式。每次用户登录或注销时,它都会调用回调。它不是用来观看特定登录的方式。一个 Promise 只能被解析一次,一个值。因此,虽然可以使用 Promise 对单个登录事件进行建模,但在查看所有登录/注销事件时是没有意义的。
所以fetchCreds 正在注册以接收有关所有登录/注销事件的通知。它不会对返回的 unsubscribe 函数做任何事情,因此它可能会监听所有此类事件,直到页面重新加载。如果您多次致电fetchCreds,它将不断添加越来越多的听众。
如果您正在等待用户完成登录,那么我建议您直接等待。 firebase.auth() 有各种以前缀 signIn 开头的方法,例如signInWithEmailAndPassword,并且这些确实会返回一个 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', {})
}
}
正如我已经提到的,这依赖于用户已经登录的假设。如果这不是一个安全的假设,那么您可能需要考虑等到登录完成后再创建需要用户的组件凭据。
更新:
来自 cmets 的问题:
如果 obj.method() 调用是异步的,并且我们确实在其中等待回调函数,这是否会确保外部异步函数 (myFn) 在内部异步函数完成之前永远不会解析?
我不完全确定你在这里问什么。
为了清楚起见,我在使用 async 和 异步 这两个词时非常小心。 setTimeout 之类的函数将被视为异步,但它不是async。
async/await 只是围绕 Promise 的大量语法糖。你并不是真的在等待一个函数,而是在等待一个 Promise。当我们谈论等待 async 函数时,我们实际上是在谈论等待它返回解决的 Promise。
因此,当您说 等待回调函数 时,并不清楚这意味着什么。你想await哪个承诺?
将async 修饰符放在函数上不会让它神奇地等待事情。它只会在遇到await 时等待。您仍然可以在 async 函数中进行其他异步调用,并且与普通函数一样,这些调用将在函数返回后执行。 “暂停”的唯一方法是 await 一个 Promise。
将await 放在另一个函数中,即使是嵌套函数,也不会影响外部函数是否等待,除非外部函数已经在等待内部函数。在幕后,这只是链接then 调用的承诺。每当您编写 await 时,您只是在向 Promise 添加另一个 then 调用。但是,除非 Promise 与外部 async 函数返回的 Promise 位于同一链中,否则这不会产生预期的效果。只需缺少一个链接,链条就会失效。
所以修改我之前的例子:
async function myFn (obj) {
await obj.method(async function () {
await somePromise
// ...
})
// ...
}
await myFn(x)
请注意,这里有 3 个函数:myFn、method 和传递给 method 的回调。问题是,await myFn(x) 会等到somePromise 吗?
从上面的代码我们实际上无法判断。这将取决于 method 在内部做什么。例如,如果method 看起来像这样,那么它仍然不起作用:
function method (callback) {
setTimeout(callback, 1000)
}
将async 放在method 上不会有任何帮助,这只会让它返回一个 Promise,但 Promise 仍然不会等待计时器触发。
我们的 Promise 链有一个断开的链接。 myFn 和回调都在创建它们的链部分,但除非 method 将这些 Promise 链接在一起,否则它将无法工作。
另一方面,如果 method 被编写为返回一个合适的等待回调完成的 Promise,那么我们将获得目标行为:
function method (callback) {
return someServerCallThatReturnsAPromise().then(callback)
}
我们本可以在这里使用async/await,但没有必要,因为我们可以直接返回 Promise。
另外,如果在 async myFn 函数中您没有返回任何内容,这是否意味着它会立即解析并且未定义?
立即这个词在这里没有很好的定义。
- 如果一个函数最后没有返回任何东西,那么它就相当于最后有
return undefined。
-
async 函数返回的 Promise 将在函数返回时解析。
- 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 4在here 3和here 5之间跳转。