Это не будет исчерпывающим ответом на ваш вопрос, но, надеюсь, это поможет вам и другим, когда вы попытаетесь прочитать документацию по $q
сервису. Мне потребовалось время, чтобы понять это.
Давайте на время отложим AngularJS и просто рассмотрим вызовы API Facebook. Оба вызова API используют механизм обратного вызова для уведомления вызывающего абонента, когда доступен ответ от Facebook:
facebook.FB.api('/' + item, function (result) {
if (result.error) {
} else {
}
});
...
Это стандартный шаблон для обработки асинхронных операций в 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) {
return 'firstResult';
};
var secondFn = function(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();
$rootScope.$apply(function() {
deferred.resolve('initial value');
});
Так что насчет обработки ошибок? До сих пор мы указали только обработчик успеха на каждом этапе цепочки. then
также принимает обработчик ошибок как необязательный второй аргумент. Вот еще один, более длинный пример цепочки обещаний, на этот раз с обработкой ошибок:
var firstFn = function(param) {
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'firstResult';
}
};
var secondFn = function(param) {
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'secondResult';
}
};
var thirdFn = function(param) {
return 'thirdResult';
};
var errorFn = function(message) {
};
var deferred = $q.defer();
var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);
Как вы можете видеть в этом примере, каждый обработчик в цепочке имеет возможность перенаправить трафик на следующий обработчик ошибок, а не на следующий обработчик успеха . В большинстве случаев у вас может быть единственный обработчик ошибок в конце цепочки, но вы также можете иметь промежуточные обработчики ошибок, которые пытаются восстановить.
Чтобы быстро вернуться к вашим примерам (и вашим вопросам), я просто скажу, что они представляют два разных способа адаптации API Facebook, ориентированного на обратный вызов, к способу наблюдения за изменениями модели в Angular. В первом примере вызов API заключен в обещание, которое может быть добавлено в область видимости и понимается системой шаблонов Angular. Второй использует более грубый подход, устанавливая результат обратного вызова непосредственно в области видимости, а затем вызывая, $scope.$digest()
чтобы Angular узнал об изменении из внешнего источника.
Эти два примера нельзя напрямую сравнивать, потому что в первом отсутствует шаг входа в систему. Однако, как правило, желательно инкапсулировать взаимодействия с внешними API, подобными этому, в отдельных сервисах и доставлять результаты контроллерам в виде обещаний. Таким образом, вы можете отделить контроллеры от внешних проблем и упростить их тестирование с помощью имитирующих сервисов.