AngularJS: где использовать обещания?

141

Я видел несколько примеров служб входа в Facebook, которые использовали обещания для доступа к FB Graph API.

Пример №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;
}

И сервисы, которые использовали, "$scope.$digest() // Manual scope evaluation"когда получили ответ

Пример №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

Вопросы следующие:

  • В чем разница в приведенных выше примерах?
  • Каковы причины и случаи использования услуги $ q ?
  • А как это работает ?
Максим
источник
9
звучит так, как будто вы должны прочитать, что такое обещания, и почему они используются в целом ... они не являются исключительными для angular, и есть много доступного материала
charlietfl
1
@charlietfl, хорошее замечание, но я ожидал, что получу сложный ответ, который будет охватывать оба вопроса: почему они используются в целом и как их использовать в Angular. Спасибо за предложение
Максим

Ответы:

401

Это не будет исчерпывающим ответом на ваш вопрос, но, надеюсь, это поможет вам и другим, когда вы попытаетесь прочитать документацию по $qсервису. Мне потребовалось время, чтобы понять это.

Давайте на время отложим AngularJS и просто рассмотрим вызовы API Facebook. Оба вызова API используют механизм обратного вызова для уведомления вызывающего абонента, когда доступен ответ от Facebook:

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

Это стандартный шаблон для обработки асинхронных операций в JavaScript и других языках.

Одна большая проблема с этим шаблоном возникает, когда вам нужно выполнить последовательность асинхронных операций, где каждая последующая операция зависит от результата предыдущей операции. Вот что делает этот код:

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

Сначала он пытается войти в систему, а затем, только убедившись, что вход был успешным, делает запрос к Graph API.

Даже в этом случае, который объединяет только две операции, все начинает запутываться. Метод askFacebookForAuthenticationпринимает обратный вызов в случае неудачи и успеха, но что происходит, когда он FB.loginуспешен, но FB.apiтерпит неудачу? Этот метод всегда вызывает successобратный вызов независимо от результата FB.apiметода.

А теперь представьте, что вы пытаетесь закодировать надежную последовательность из трех или более асинхронных операций таким образом, чтобы правильно обрабатывать ошибки на каждом этапе и быть понятными для всех или даже для вас через несколько недель. Возможно, но очень легко просто продолжать встраивать эти обратные вызовы и при этом терять следы ошибок.

Теперь давайте отложим на время API Facebook и просто рассмотрим API Angular Promises, реализованный $qслужбой. Шаблон, реализованный этой службой, представляет собой попытку превратить асинхронное программирование обратно в нечто, напоминающее линейную серию простых операторов, с возможностью «выбросить» ошибку на любом этапе пути и обработать ее в конце, семантически аналогично знакомый try/catchблок.

Рассмотрим этот надуманный пример. Скажем, у нас есть две функции, где вторая функция потребляет результат первой:

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

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

 secondFn(firstFn()); 

Теперь представьте, что для выполнения firstFn и secondFn требуется много времени, поэтому мы хотим обрабатывать эту последовательность асинхронно. Сначала мы создаем новый deferredобъект, который представляет собой цепочку операций:

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

promiseСвойство представляет конечный результат цепочки. Если вы зарегистрируете обещание сразу после его создания, вы увидите, что это просто пустой объект ( {}). Пока ничего не видно, двигайтесь дальше.

Пока наше обещание представляет собой только начальную точку в цепочке. Теперь добавим две наши операции:

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

thenМетод добавляет шаг к цепочке , а затем возвращает новое обещание , представляющее возможный результат расширенной цепочки. Вы можете добавить столько шагов, сколько захотите.

Пока что мы настроили нашу цепочку функций, но на самом деле ничего не произошло. Вы начинаете работу с вызова deferred.resolve, указывая начальное значение, которое вы хотите передать первому действительному шагу в цепочке:

 deferred.resolve('initial value');

А потом ... по-прежнему ничего не происходит. Чтобы гарантировать правильное наблюдение за изменениями модели, Angular фактически не вызывает первый шаг в цепочке, пока не $applyбудет вызван следующий раз :

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

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

Так что насчет обработки ошибок? До сих пор мы указали только обработчик успеха на каждом этапе цепочки. thenтакже принимает обработчик ошибок как необязательный второй аргумент. Вот еще один, более длинный пример цепочки обещаний, на этот раз с обработкой ошибок:

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

Как вы можете видеть в этом примере, каждый обработчик в цепочке имеет возможность перенаправить трафик на следующий обработчик ошибок, а не на следующий обработчик успеха . В большинстве случаев у вас может быть единственный обработчик ошибок в конце цепочки, но вы также можете иметь промежуточные обработчики ошибок, которые пытаются восстановить.

Чтобы быстро вернуться к вашим примерам (и вашим вопросам), я просто скажу, что они представляют два разных способа адаптации API Facebook, ориентированного на обратный вызов, к способу наблюдения за изменениями модели в Angular. В первом примере вызов API заключен в обещание, которое может быть добавлено в область видимости и понимается системой шаблонов Angular. Второй использует более грубый подход, устанавливая результат обратного вызова непосредственно в области видимости, а затем вызывая, $scope.$digest()чтобы Angular узнал об изменении из внешнего источника.

Эти два примера нельзя напрямую сравнивать, потому что в первом отсутствует шаг входа в систему. Однако, как правило, желательно инкапсулировать взаимодействия с внешними API, подобными этому, в отдельных сервисах и доставлять результаты контроллерам в виде обещаний. Таким образом, вы можете отделить контроллеры от внешних проблем и упростить их тестирование с помощью имитирующих сервисов.

Карлголд
источник
5
Думаю, это отличный ответ! Для меня главным было описать общий случай, когда обещание действительно актуально. Честно говоря, я надеялся на другой реальный пример (например, с Facebook), но, думаю, это тоже работает. Огромное спасибо!
Максим
2
Альтернативой объединению нескольких thenметодов является использование $q.all. Краткое руководство по этому поводу можно найти здесь .
Богдан
2
$q.allподходит, если вам нужно дождаться завершения нескольких независимых асинхронных операций. Он не заменяет связывание, если каждая операция зависит от результата предыдущей операции.
karlgold
1
здесь лаконично объясняется цепочка then. Помогло мне понять и использовать его в полной мере. Спасибо
Тушар Джоши
1
Отличный ответ @karlgold! У меня есть один вопрос. Если в последнем фрагменте кода вы измените return 'firstResult'часть на return $q.resolve('firstResult'), какая будет разница?
technophyle
9

Я ожидал сложного ответа, который охватит и то, и другое: почему они используются в целом и как их использовать в Angular.

Это основа для Angular Promises MVP (минимально жизнеспособное обещание) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

Источник:

(для тех, кому лень переходить по ссылкам)

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

  });

(Я знаю, что это не решает ваш конкретный пример с Facebook, но я считаю полезными следующие фрагменты)

Через: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


Обновление от 28 февраля 2014 г. Начиная с версии 1.2.0, обещания больше не разрешаются с помощью шаблонов. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(в примере плункера используется версия 1.1.5.)

Марс Робертсон
источник
afaik, мы любим так, потому что мы ленивы
mkb
это помогло мне понять $ q, отложенные и связанные вызовы функций .then, так что спасибо.
aliopi 08
1

Отложенный представляет собой результат асинхронной операции. Он предоставляет интерфейс, который может использоваться для сигнализации состояния и результата операции, которую он представляет. Он также предоставляет способ получить связанный экземпляр обещания.

Промис предоставляет интерфейс для взаимодействия с относящимся к нему отложенной операцией и, таким образом, позволяет заинтересованным сторонам получить доступ к состоянию и результату отложенной операции.

При создании отложенного состояния оно находится в состоянии ожидания и не имеет результата. Когда мы разрешаем () или reject () отложенный, он меняет свое состояние на разрешено или отклонено. Тем не менее, мы можем получить связанное обещание сразу после создания отложенного и даже назначить взаимодействия с его будущим результатом. Эти взаимодействия будут происходить только после того, как отложенные отклонены или разрешены.

Ram G
источник
1

используйте обещание в контроллере и убедитесь, что данные доступны или нет

 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>

Маниваннан А
источник