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