AngularJS: ¿Dónde usar las promesas?

141

Vi algunos ejemplos de servicios de inicio de sesión de Facebook que usaban promesas para acceder a FB Graph API.

Ejemplo # 1 :

this.api = function(item) {
  var deferred = $q.defer();
  if (item) {
    facebook.FB.api('/' + item, function (result) {
      $rootScope.$apply(function () {
        if (angular.isUndefined(result.error)) {
          deferred.resolve(result);
        } else {
          deferred.reject(result.error);
        }
      });
    });
  }
  return deferred.promise;
}

Y los servicios que se usaron "$scope.$digest() // Manual scope evaluation"cuando obtuvieron la respuesta

Ejemplo # 2 :

angular.module('HomePageModule', []).factory('facebookConnect', function() {
    return new function() {
        this.askFacebookForAuthentication = function(fail, success) {
            FB.login(function(response) {
                if (response.authResponse) {
                    FB.api('/me', success);
                } else {
                    fail('User cancelled login or did not fully authorize.');
                }
            });
        }
    }
});

function ConnectCtrl(facebookConnect, $scope, $resource) {

    $scope.user = {}
    $scope.error = null;

    $scope.registerWithFacebook = function() {
        facebookConnect.askFacebookForAuthentication(
        function(reason) { // fail
            $scope.error = reason;
        }, function(user) { // success
            $scope.user = user
            $scope.$digest() // Manual scope evaluation
        });
    }
}

JSFiddle

Las preguntas son:

  • ¿Cuál es la diferencia en los ejemplos anteriores?
  • ¿Cuáles son las razones y los casos para usar el servicio $ q ?
  • Y cómo funciona ?
Maksym
fuente
9
Parece que debería leer cuáles son las promesas y por qué se usan en general ... no son exclusivas de angular y hay mucho material disponible
charlietfl
1
@charlietfl, buen punto, pero esperaba una respuesta compleja que cubriera ambos: por qué se usan en general y cómo usarlo en Angular. Gracias por su sugerencia
Maksym

Respuestas:

401

Esta no será una respuesta completa a su pregunta, pero espero que esto lo ayude a usted y a otros cuando intente leer la documentación del $qservicio. Me tomó un tiempo entenderlo.

Dejemos de lado AngularJS por un momento y solo consideremos las llamadas API de Facebook. Ambas llamadas API utilizan un mecanismo de devolución de llamada para notificar a la persona que llama cuando la respuesta de Facebook está disponible:

  facebook.FB.api('/' + item, function (result) {
    if (result.error) {
      // handle error
    } else {
      // handle success
    }
  });
  // program continues while request is pending
  ...

Este es un patrón estándar para manejar operaciones asincrónicas en JavaScript y otros idiomas.

Un gran problema con este patrón surge cuando necesita realizar una secuencia de operaciones asincrónicas, donde cada operación sucesiva depende del resultado de la operación anterior. Eso es lo que está haciendo este código:

  FB.login(function(response) {
      if (response.authResponse) {
          FB.api('/me', success);
      } else {
          fail('User cancelled login or did not fully authorize.');
      }
  });

Primero intenta iniciar sesión, y solo después de verificar que el inicio de sesión se realizó correctamente, realiza la solicitud a la API Graph.

Incluso en este caso, que solo está encadenando dos operaciones, las cosas comienzan a complicarse. El método askFacebookForAuthenticationacepta una devolución de llamada por falla y éxito, pero ¿qué sucede cuando FB.logintiene éxito pero FB.apifalla? Este método siempre invoca la successdevolución de llamada independientemente del resultado del FB.apimétodo.

Ahora imagine que está tratando de codificar una secuencia robusta de tres o más operaciones asincrónicas, de una manera que maneje adecuadamente los errores en cada paso y sea legible para cualquier otra persona o incluso para usted después de unas pocas semanas. Es posible, pero es muy fácil seguir anidando esas devoluciones de llamada y perder la noción de errores en el camino.

Ahora, dejemos de lado la API de Facebook por un momento y consideremos la API de Promesas Angulares, tal como la implementa el $qservicio. El patrón implementado por este servicio es un intento de convertir la programación asincrónica en algo parecido a una serie lineal de declaraciones simples, con la capacidad de 'arrojar' un error en cualquier paso del camino y manejarlo al final, semánticamente similar al try/catchBloque familiar .

Considere este ejemplo artificial. Digamos que tenemos dos funciones, donde la segunda función consume el resultado de la primera:

 var firstFn = function(param) {
    // do something with param
    return 'firstResult';
 };

 var secondFn = function(param) {
    // do something with param
    return 'secondResult';
 };

 secondFn(firstFn()); 

Ahora imagine que firstFn y secondFn tardan mucho tiempo en completarse, por lo que queremos procesar esta secuencia de forma asincrónica. Primero creamos un nuevo deferredobjeto, que representa una cadena de operaciones:

 var deferred = $q.defer();
 var promise = deferred.promise;

La promisepropiedad representa el resultado final de la cadena. Si registra una promesa inmediatamente después de crearla, verá que es solo un objeto vacío ( {}). Aún no hay nada que ver, muévete.

Hasta ahora, nuestra promesa solo representa el punto de partida de la cadena. Ahora agreguemos nuestras dos operaciones:

 promise = promise.then(firstFn).then(secondFn);

El thenmétodo agrega un paso a la cadena y luego devuelve una nueva promesa que representa el resultado final de la cadena extendida. Puede agregar tantos pasos como desee.

Hasta ahora, hemos configurado nuestra cadena de funciones, pero en realidad no ha pasado nada. Empiezas las cosas llamando deferred.resolve, especificando el valor inicial que deseas pasar al primer paso real de la cadena:

 deferred.resolve('initial value');

Y entonces ... todavía no pasa nada. Para garantizar que los cambios en el modelo se observen correctamente, Angular en realidad no llama al primer paso en la cadena hasta que $applyse llame la próxima vez :

 deferred.resolve('initial value');
 $rootScope.$apply();

 // or     
 $rootScope.$apply(function() {
    deferred.resolve('initial value');
 });

Entonces, ¿qué pasa con el manejo de errores? Hasta ahora solo hemos especificado un controlador de éxito en cada paso de la cadena. thenTambién acepta un controlador de errores como un segundo argumento opcional. Aquí hay otro ejemplo más largo de una cadena de promesa, esta vez con manejo de errores:

 var firstFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'firstResult';
    }
 };

 var secondFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'secondResult';
    }
 };

 var thirdFn = function(param) {
    // do something with param
    return 'thirdResult';
 };

 var errorFn = function(message) {
   // handle error
 };

 var deferred = $q.defer();
 var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);

Como puede ver en este ejemplo, cada controlador de la cadena tiene la oportunidad de desviar el tráfico al siguiente controlador de errores en lugar del siguiente controlador de éxito . En la mayoría de los casos, puede tener un único controlador de errores al final de la cadena, pero también puede tener controladores de errores intermedios que intenten la recuperación.

Para volver rápidamente a sus ejemplos (y sus preguntas), solo diré que representan dos formas diferentes de adaptar la API orientada a la devolución de llamadas de Facebook a la forma en que Angular observa los cambios en el modelo. El primer ejemplo envuelve la llamada API en una promesa, que se puede agregar a un alcance y se entiende por el sistema de plantillas de Angular. El segundo adopta el enfoque de fuerza más bruta de establecer el resultado de devolución de llamada directamente en el alcance y luego llamar $scope.$digest()para que Angular sea consciente del cambio desde una fuente externa.

Los dos ejemplos no son directamente comparables, porque al primero le falta el paso de inicio de sesión. Sin embargo, generalmente es deseable encapsular interacciones con API externas como esta en servicios separados y entregar los resultados a los controladores como promesas. De esa manera, puede mantener sus controladores separados de las preocupaciones externas y probarlos más fácilmente con servicios simulados.

karlgold
fuente
55
Creo que es una gran respuesta! Lo principal, para mí, fue describir un caso general cuando la promesa es realmente real. Honestamente, esperaba otro ejemplo real (como en Facebook), pero esto también funciona, supongo. ¡Muchas gracias!
Maksym
2
Una alternativa para encadenar múltiples thenmétodos es usar $q.all. Un tutorial rápido sobre eso se puede encontrar aquí .
Bogdan
2
$q.alles apropiado si necesita esperar a que se completen varias operaciones asincrónicas independientes . No reemplaza el encadenamiento si cada operación depende del resultado de la operación anterior.
karlgold
1
El encadenamiento de entonces se explica sucintamente aquí. Me ayudó a entenderlo y usarlo en todo su potencial. Gracias
Tushar Joshi
1
Gran respuesta @karlgold! Tengo una pregunta. Si, en el último fragmento de código, cambia la return 'firstResult'parte a return $q.resolve('firstResult'), ¿cuál será la diferencia?
technophyle
9

Esperaba una respuesta compleja que cubriera ambos: por qué se usan en general y cómo usarlo en Angular

Este es el punto clave para las promesas angulares MVP (promesa mínima viable) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

Fuente:

(para aquellos demasiado flojos para hacer clic en los enlaces)

index.html

  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-app="myModule" ng-controller="HelloCtrl">
    <h1>Messages</h1>
    <ul>
      <li ng-repeat="message in messages">{{ message }}</li>
    </ul>
  </body>

</html>

app.js

angular.module('myModule', [])

  .factory('HelloWorld', function($q, $timeout) {

    var getMessages = function() {
      var deferred = $q.defer();

      $timeout(function() {
        deferred.resolve(['Hello', 'world']);
      }, 2000);

      return deferred.promise;
    };

    return {
      getMessages: getMessages
    };

  })

  .controller('HelloCtrl', function($scope, HelloWorld) {

    $scope.messages = HelloWorld.getMessages();

  });

(Sé que no resuelve su ejemplo específico de Facebook, pero creo que los siguientes fragmentos son útiles)

Vía: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


Actualización 28 de febrero de 2014: a partir de 1.2.0, las promesas ya no se resuelven mediante plantillas. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(el ejemplo de plunker usa 1.1.5.)

Mars Robertson
fuente
afaik nos encanta porque somos flojos
mkb
esto me ayudó a comprender $ q, diferido y encadenado. luego llamadas a funciones, así que gracias.
aliopi
1

Un diferido representa el resultado de una operación asincrónica. Expone una interfaz que se puede utilizar para señalar el estado y el resultado de la operación que representa. También proporciona una forma de obtener la instancia de promesa asociada.

Una promesa proporciona una interfaz para interactuar con sus diferidos relacionados y, por lo tanto, permite que las partes interesadas tengan acceso al estado y al resultado de la operación diferida.

Al crear un diferido, su estado está pendiente y no tiene ningún resultado. Cuando resolvemos () o rechazamos () lo diferido, cambia su estado a resuelto o rechazado. Aún así, podemos obtener la promesa asociada inmediatamente después de crear un resultado diferido e incluso asignar interacciones con su resultado futuro. Esas interacciones ocurrirán solo después de que el aplazado sea rechazado o resuelto.

Ram G
fuente
1

use la promesa dentro de un controlador y asegúrese de que los datos estén disponibles o no

 var app = angular.module("app",[]);
      
      app.controller("test",function($scope,$q){
        var deferred = $q.defer();
        deferred.resolve("Hi");
        deferred.promise.then(function(data){
        console.log(data);    
        })
      });
      angular.bootstrap(document,["app"]);
<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
  </head>

  <body>
    <h1>Hello Angular</h1>
    <div ng-controller="test">
    </div>
  </body>

</html>

Manivannan A
fuente