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