¿Por qué Firebase pierde referencia fuera de la función once()?
Estoy usando Firebase junto con angularJS para obtener una lista de usuarios. Puedo leer todos los usuarios de mi base de datos con la once()
función, pero no puedo entender por qué userList
vuelve indefinido a continuación
.service('userService', [function() {
this.getUsers = function() {
var users;
var userList;
var ref = firebase.database().ref('/users/');
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
}).then(function(){
userList = users;
console.log(userList); // outputs all users
},(function(error){
alert('error: ' + error);
});
console.log(userList); // outputs 'undefined'
}
}]);
Espero para asignar mi userList
variable hasta que termine de procesar users
, pero no tuve suerte.
Hay algo importante aquí que me falta en lo que respecta a las promesas/devoluciones de llamada y no puedo encontrarlo en la documentación; ¿Alguien podría ayudarme a entender mi problema?
El problema es (como dice imjared) que los datos se leen desde Firebase de forma asincrónica. Entonces el código no se ejecuta en el orden que crees. Es más fácil verlo simplificándolo con solo unas pocas declaraciones de registro:
.service('userService', [function() {
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
console.log("before attaching listener");
ref.once('value').then(function(snapshot) {
console.log("got value");
});
console.log("after attaching listener");
}
}]);
El resultado de esto será:
antes de adjuntar el oyente
después de adjuntar el oyente
tengo valor
Saber el orden en el que se ejecuta esto debería explicar perfectamente por qué no puedes imprimir la lista de usuarios después de haber adjuntado el oyente. Si no, recomiendo leer también esta gran respuesta: Cómo devolver la respuesta de una llamada asincrónica
Ahora la solución: necesitarás usar la lista de usuarios en la devolución de llamada o devolver una promesa.
Utilice la lista de usuarios en la devolución de llamada
Esta es la forma más antigua de lidiar con código asincrónico: mover todo el código que necesita la lista de usuarios a la devolución de llamada.
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
}
console.log(users); // outputs all users
})
Está reformulando su código de "primero cargue la lista de usuarios, luego imprima su contenido" a "cada vez que se cargue la lista de usuarios, imprima su contenido". La diferencia en la definición es menor, pero de repente estás perfectamente equipado para lidiar con la carga asincrónica.
También puedes hacer lo mismo con una promesa, como lo haces en tu código:
ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
});
Pero usar una promesa tiene una gran ventaja sobre usar la devolución de llamada directamente: puedes devolver una promesa .
devolver una promesa
A menudo no querrás poner todo el código que necesitan los usuarios en la getUsers()
función. En ese caso, puedes pasar una devolución de llamada getUsers()
(que no mostraré aquí, pero es muy similar a la devolución de llamada a la que puedes pasar once()
) o puedes devolver una promesa desde getUsers()
:
this.getUsers = function() {
var ref = firebase.database().ref('/users/');
return ref.once('value').then(function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
return(users);
}).catch(function(error){
alert('error: ' + error);
});
}
Con este servicio, ahora podemos llamar getUsers()
y usar la promesa resultante para llegar a los usuarios una vez que estén cargados:
userService.getUsers().then(function(userList) {
console.log(userList);
})
Y con eso has domesticado a la bestia asincrónica. Bueno... al menos por ahora. Esto seguirá confundiendo incluso a los desarrolladores de JavaScript experimentados de vez en cuando, así que no se preocupe si le lleva algún tiempo acostumbrarse.
Utilice async y espere
Ahora que la función devuelve una promesa, puedes usar async
/ await
para hacer que la llamada final desde arriba parezca un poco más familiar:
function getAndLogUsers() async {
const userList = await userService.getUsers();
console.log(userList);
}
Puede ver que este código parece casi una llamada sincrónica, gracias al uso de la await
palabra clave. Pero para poder usarlo, tenemos que marcar getAndLogUsers
(o cualquiera que sea el ámbito principal donde usamos await
) como async
, lo que significa que también devuelve a Future
. Por lo tanto, cualquiera que llame getAndLogUsers
deberá ser consciente de su naturaleza asincrónica.
Necesitas pensar asíncrono:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
console.log(users); // outputs 'undefined'
}
}]);
Apuesto que si hicieras esto, estarías bien:
.service('userService', [function() {
this.getUsers = function() {
var users;
var ref = firebase.database().ref('/users/');
<!-- this happens ASYNC -->
ref.once('value', function(snapshot) {
users = snapshot.val();
for(var key in users) {
users[key].id = key;
// do some other stuff
}
console.log(users); // outputs all users
},function(error){
alert('error: ' + error);
});
<!-- end async action -->
window.setTimeout( function() {
console.log(users); // outputs 'undefined'
}, 1500 );
}
}]);
Aclaración según el comentario de Franks:
Debo aclarar además que esto setTimeout
simplemente demostraría que se trata de una cuestión de tiempo. Si usa setTimeout en su aplicación, probablemente lo pasará mal ya que no puede esperar de manera confiable n milisegundos para obtener resultados, necesita obtenerlos y luego realizar una devolución de llamada o resolver una promesa después de haber obtenido la instantánea de los datos.