AngularJS: Onde usar promessas?

141

Vi alguns exemplos de serviços de login do Facebook que estavam usando promessas para acessar a API do FB Graph.

Exemplo 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;
}

E serviços usados "$scope.$digest() // Manual scope evaluation"quando obtivemos a resposta

Exemplo 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

As perguntas são:

  • Qual é a diferença nos exemplos acima?
  • Quais são os motivos e casos para usar o serviço $ q ?
  • E como isso funciona ?
Maksym
fonte
9
Parece que você deve ler sobre o que promessas são, e por que eles são usados em geral ... eles não são exclusivos para angular e há muitos disponíveis materiais
charlietfl
1
@ charlietfl, bom ponto, mas eu esperava uma resposta complexa que cubra ambos: por que eles são usados ​​em geral e como usá-lo no Angular. Obrigado por sua sugestão
Maksym

Respostas:

401

Essa não será uma resposta completa para sua pergunta, mas espero que isso ajude você e outras pessoas quando tentar ler a documentação no $qserviço. Demorei um pouco para entender.

Vamos anular o AngularJS por um momento e considerar as chamadas da API do Facebook. As duas chamadas de API usam um mecanismo de retorno de chamada para notificar o chamador quando a resposta do Facebook estiver disponível:

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

Este é um padrão padrão para lidar com operações assíncronas em JavaScript e outros idiomas.

Um grande problema com esse padrão surge quando você precisa executar uma sequência de operações assíncronas, em que cada operação sucessiva depende do resultado da operação anterior. É isso que este código está fazendo:

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

Primeiro, ele tenta efetuar login e, somente depois de verificar se o logon foi bem-sucedido, faz a solicitação à API do Graph.

Mesmo neste caso, que está encadeando apenas duas operações, as coisas começam a ficar confusas. O método askFacebookForAuthenticationaceita um retorno de chamada por falha e sucesso, mas o que acontece quando FB.loginé bem-sucedido, mas FB.apifalha? Este método sempre chama o successretorno de chamada, independentemente do resultado do FB.apimétodo.

Agora imagine que você está tentando codificar uma sequência robusta de três ou mais operações assíncronas, de maneira que lide adequadamente com os erros a cada etapa e seja legível para qualquer outra pessoa ou mesmo para você depois de algumas semanas. Possível, mas é muito fácil continuar aninhando esses retornos de chamada e perder o controle de erros ao longo do caminho.

Agora, vamos anular a API do Facebook por um momento e considerar a API de Promessas Angulares, conforme implementada pelo $qserviço. O padrão implementado por este serviço é uma tentativa de transformar a programação assíncrona novamente em algo semelhante a uma série linear de instruções simples, com a capacidade de 'lançar' um erro em qualquer etapa do processo e manipulá-lo no final, semanticamente semelhante ao try/catchbloco familiar .

Considere este exemplo artificial. Digamos que temos duas funções, em que a segunda função consome o resultado da primeira:

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

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

 secondFn(firstFn()); 

Agora imagine que firstFn e secondFn demoram muito tempo para serem concluídos, portanto, queremos processar essa sequência de forma assíncrona. Primeiro, criamos um novo deferredobjeto, que representa uma cadeia de operações:

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

A promisepropriedade representa o resultado final da cadeia. Se você registrar uma promessa imediatamente após criá-la, verá que é apenas um objeto vazio ( {}). Nada a ver ainda, siga em frente.

Até agora, nossa promessa representa apenas o ponto de partida da cadeia. Agora vamos adicionar nossas duas operações:

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

O thenmétodo adiciona uma etapa à cadeia e, em seguida, retorna uma nova promessa que representa o resultado final da cadeia estendida. Você pode adicionar quantas etapas desejar.

Até agora, configuramos nossa cadeia de funções, mas nada realmente aconteceu. Você começa chamando deferred.resolve, especificando o valor inicial que deseja passar para a primeira etapa real da cadeia:

 deferred.resolve('initial value');

E então ... ainda nada acontece. Para garantir que as alterações do modelo sejam observadas corretamente, o Angular não chama realmente a primeira etapa da cadeia até que a próxima vez $applyseja chamada:

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

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

E o tratamento de erros? Até agora, especificamos apenas um manipulador de sucesso em cada etapa da cadeia. thentambém aceita um manipulador de erros como um segundo argumento opcional. Aqui está outro exemplo mais longo de uma cadeia de promessas, desta vez com tratamento de erros:

 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 você pode ver neste exemplo, cada manipulador da cadeia tem a oportunidade de desviar o tráfego para o próximo manipulador de erros em vez do próximo manipulador de sucesso . Na maioria dos casos, você pode ter um único manipulador de erros no final da cadeia, mas também pode ter manipuladores de erros intermediários que tentam recuperar.

Para retornar rapidamente aos seus exemplos (e às suas perguntas), vou apenas dizer que eles representam duas maneiras diferentes de adaptar a API orientada a retorno de chamada do Facebook à maneira da Angular de observar alterações de modelo. O primeiro exemplo envolve a chamada da API em uma promessa, que pode ser adicionada a um escopo e é entendida pelo sistema de modelagem da Angular. O segundo adota a abordagem de força bruta de definir o resultado do retorno de chamada diretamente no escopo e depois chamar $scope.$digest()para tornar o Angular ciente da mudança de uma fonte externa.

Os dois exemplos não são diretamente comparáveis, porque o primeiro está faltando na etapa de login. No entanto, geralmente é desejável encapsular interações com APIs externas como esta em serviços separados e entregar os resultados aos controladores como promessas. Dessa forma, você pode manter seus controladores separados das preocupações externas e testá-los mais facilmente com serviços simulados.

karlgold
fonte
5
Eu acho que é uma ótima resposta! O principal, para mim, foi descrever casos gerais em que a promessa é realmente real. Honestamente, eu estava esperando por outro exemplo real (como no Facebook), mas isso também funciona, eu acho. Muito Obrigado!
Maksym
2
Uma alternativa para encadear vários thenmétodos é usar $q.all. Um tutorial rápido sobre isso pode ser encontrado aqui .
Bogdan
2
$q.allé apropriado se você precisar aguardar a conclusão de várias operações assíncronas independentes . Ele não substitui o encadeamento se cada operação depender do resultado da operação anterior.
karlgold
1
o encadeamento de então é explicado sucintamente aqui. Me ajudou a entender e usá-lo em todo o seu potencial. Obrigado
Tushar Joshi
1
Ótima resposta @karlgold! Eu tenho uma pergunta. Se, no último trecho de código, você alterar a return 'firstResult'parte para return $q.resolve('firstResult'), qual será a diferença?
technophyle
9

Eu esperava uma resposta complexa que cubra ambos: por que eles são usados ​​em geral e como usá-lo no Angular

Esta é a chave para o MVP de promessas angulares (promessa mínima viável) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

Fonte:

(para aqueles com preguiça de clicar nos links)

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();

  });

(Eu sei que isso não resolve o seu exemplo específico do Facebook, mas acho úteis os seguintes trechos)

Via: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


Atualização 28 de fevereiro de 2014: A partir da versão 1.2.0, as promessas não são mais resolvidas pelos modelos. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(o exemplo de plunker usa 1.1.5.)

Mars Robertson
fonte
afaik nós amamos isso porque somos preguiçosos
mkb
isso me ajudou a entender o $ q, adiado e encadeado. em seguida, chamadas de função, então obrigado.
aliopi
1

Um adiado representa o resultado de uma operação assíncrona. Expõe uma interface que pode ser usada para sinalizar o estado e o resultado da operação que representa. Ele também fornece uma maneira de obter a instância de promessa associada.

Uma promessa fornece uma interface para interagir com o adiado relacionado e, portanto, permite que as partes interessadas tenham acesso ao estado e ao resultado da operação adiada.

Ao criar um adiado, seu estado está pendente e não tem nenhum resultado. Quando resolvemos () ou rejeitamos () o adiado, ele muda seu estado para resolvido ou rejeitado. Ainda assim, podemos obter a promessa associada imediatamente após criar um adiado e até atribuir interações com seu resultado futuro. Essas interações ocorrerão somente após o adiado rejeitado ou resolvido.

Ram G
fonte
1

use a promessa dentro de um controlador e verifique se os dados estão disponíveis ou não

 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
fonte