AngularJS: Де використовувати обіцянки?


141

Я побачив кілька прикладів служб входу в Facebook, які використовували обіцянки отримати доступ до API Graph Graph.

Приклад №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
звучить, як вам слід прочитати, що таке обіцянки, і чому вони використовуються взагалі ... вони не виняткові для кутових, і є багато доступних матеріалів
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.');
      }
  });

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

Навіть у цьому випадку, який поєднує лише дві операції, речі починають псуватися. Метод 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()); 

Тепер уявімо, що для першогоFn і SecondFn потрібно тривати багато часу, тому ми хочемо обробити цю послідовність асинхронно. Спочатку ми створюємо новий deferredоб'єкт, який представляє ланцюг операцій:

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

promiseВластивість представляє кінцевий результат ланцюжка. Якщо ви ввійдете в обіцянку відразу після створення, ви побачите, що це просто порожній об’єкт ({} ). Немає нічого бачити, рухайтеся прямо вздовж.

Поки наша обіцянка є лише відправною точкою ланцюга. Тепер додамо наші дві операції:

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

The 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
ланцюжок тоді пояснюється тут лаконічно. Допоміг мені зрозуміти і використати його на повний потенціал. Спасибі
Тушар Джоші

1
Чудова відповідь @karlgold! У мене одне питання. Якщо в останньому фрагменті коду ви поміняєте return 'firstResult'частину на return $q.resolve('firstResult'), яка буде різниця?
технофіл

9

Я очікував складної відповіді, яка охоплюватиме обидва: чому вони використовуються взагалі та як їх використовувати в 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

(у прикладі plunker використовується 1.1.5.)


afaik ми любимо так, тому що ми ліниві
mkb

це допомогло мені зрозуміти $ q, відкладені та приковані. тоді дзвінки функцій, тож спасибі.
aliopi

1

Відкладене відображає результат асинхронної операції. Він розкриває інтерфейс, який може використовуватися для сигналізації стану та результату операції, яку він представляє. Він також пропонує спосіб отримати пов'язаний екземпляр обіцянки.

Обіцянка надає інтерфейс для взаємодії з пов'язаними з нею відкладеними, і таким чином, дозволяє зацікавленим сторонам отримати доступ до стану та результату відкладеної операції.

Під час створення відкладеного стану стан очікує на розгляд, і це не має жодного результату. Коли ми вирішуємо () або відхиляємо () відкладене, воно змінює його стан на вирішене або відхилене. Тим не менш, ми можемо отримати відповідне обіцянку відразу після створення відкладеного і навіть призначити взаємодії з його майбутнім результатом. Ці взаємодії відбудуться лише після відкладеного відхилення чи вирішення.


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>

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.