AngularJS: где использовать промисы?

Я видел несколько примеров служб входа в Facebook, которые использовали promises для доступа к 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?
  • И как это работает?

person Maksym    schedule 24.03.2013    source источник
comment
похоже, вам следует прочитать о том, что такое обещания и почему они используются в целом... они не являются эксклюзивными для angular, и есть много доступного материала   -  person charlietfl    schedule 25.03.2013
comment
@charlietfl, хороший вопрос, но я ожидал сложного ответа, который будет охватывать оба вопроса: почему они вообще используются и как их использовать в Angular. Спасибо за ваше предложение   -  person Maksym    schedule 01.04.2013


Ответы (4)


Это не будет полным ответом на ваш вопрос, но, надеюсь, он поможет вам и другим, когда вы попытаетесь прочитать документацию по сервису $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 в отдельные службы и доставлять результаты контроллерам в виде промисов. Таким образом, вы можете отделить свои контроллеры от внешних проблем и упростить их тестирование с помощью фиктивных сервисов.

person karlgold    schedule 01.04.2013
comment
Я думаю, что это отличный ответ! Главное для меня было описать общий случай, когда обещание действительно актуально. Честно говоря, я надеялся на другой реальный пример (как с Facebook), но, думаю, это тоже работает. Огромное спасибо! - person Maksym; 01.04.2013
comment
Я попытался зарегистрировать промис сразу после его создания (в Angular v1.1.5), и это не был пустой объект. Это был объект с двумя функциями: then() и always(). - person Mark Rajcok; 29.06.2013
comment
Эта статья очень полезна, спасибо - person schwertfisch; 13.02.2014
comment
Жаль, что я могу +1 только один раз! - person rocketboy; 07.03.2014
comment
Альтернативой объединению нескольких методов then является использование $q.all. Краткое руководство по этому вопросу можно найти здесь. - person Bogdan; 10.07.2014
comment
$q.all подходит, если вам нужно дождаться завершения нескольких независимых асинхронных операций. Он не заменяет цепочку, если каждая операция зависит от результата предыдущей операции. - person karlgold; 10.07.2014
comment
Цепочка then кратко объясняется здесь. Помогли мне понять и использовать его в полной мере. Спасибо - person Tushar Joshi; 15.01.2015
comment
Отличный ответ @karlgold! У меня есть один вопрос. Если в последнем фрагменте кода вы замените часть return 'firstResult' на return $q.resolve('firstResult'), в чем будет разница? - person technophyle; 30.11.2015
comment
Люблю этот ответ! +1 - person Fergus; 26.04.2016

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

Это план для угловых обещаний 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.)

person Mars Robertson    schedule 25.01.2014
comment
афаик мы любим так потому что ленивы - person mkb; 18.08.2016
comment
это помогло мне понять $q, отложенные и связанные вызовы функций .then, так что спасибо. - person aliopi; 08.08.2017

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

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

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

person Ram G    schedule 06.08.2015

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

 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>

person Manivannan A    schedule 27.01.2017