Одной из ключевых составляющих практически любого веб-приложения является взаимодействие с сервером. В больших приложениях это далеко не один запрос. При этом запросы часто необходимо объединять для последовательного или параллельного выполнения, а часто сочетать и то и другое. Кроме того, большие приложения обычно имеют многослойную архитектуру — обертка над RESTFul API => бизнес-сущности => более комплексная бизнес-логика (разбиение условно для примера). И на каждом слое необходимо принять данные в одном формате и передать на следующий слой уже в другом.
Вот со всеми этими задачами могут помочь справиться Promise-ы.
За подробностями добро пожаловать под кат.
Promise-ы предоставляют интерфейс для взаимодействия с объектами, содержащими результат выполнения некоторой операции, время окончания которой неизвестно. Изначально любой promise не разрешен (unresolved) и будет разрешен либо с определенным значением (resolved), либо отвергнут с ошибкой (rejected). Как только promise становится разрешен или отвергнут, его состояние уже не может измениться, что обеспечивает неизменность состояния в течение какого угодно числа проверок. Что не означает, что на разных этапах проверок вы получите одно и тоже значение.
Кроме того, promise-ы можно объединять как для последовательного, так и для параллельного исполнения.
Далее все описание будет построено на базе AngularJS 1.1.5, а все примеры будут исполнены в виде тестов.
Итак, что из себя представляет promise? Это объект с двумя методами:
Что в AngularJS вернет вам promise?
Далее последовательно пройдемся по всем вариантам работы с promise-ами.
Ничего интересного — callback и все. Но надо же от чего-то отталкиваться в изложении?.. :-)
Благодаря такой цепочке
Любое возвращаемое значение из
Чтобы сделать
Это позволит избежать пирамидального кода, сделать код более линейным, а значит более читабельным и простым в поддержке.
Обернет любой объект в promise-объект. Чаще всего необходимо для моков в юнит-тестах. Или когда есть цепочка запросов, один из которых нужно выполнять не всегда, а только в некоторых случаях, а в остальных случаях есть уже готовый объект.
Обратите внимание, что для разрешения promise-ов должен быть выполнен хотя бы один
Создание своих
Ключевые моменты здесь:
Во-первых, $q-сервис реализован с учетом dirty-checking в AngularJS, что дает более быстрые resolve и reject, убирая ненужные перерисовки браузером.
Во-вторых, при интерполяции и вычислении angular-выражений promise-объекты интерпретируются как значение, полученное после resolve.
Это означает, что если следить за изменением переменной через строковый
Исключение составляет использование функции. Возвращаемое из нее значение будет использовано как есть, т.е. это будет именно promise-объект.
Понимание этого важно при написании различных loading-виджетов, loading-директив для кнопок и т.п.
И напоследок скриншот выполненных тестов из WebStorm 7 EAP. Все же приятную добавили интеграцию с karma.

Вот со всеми этими задачами могут помочь справиться Promise-ы.
За подробностями добро пожаловать под кат.
Promise-ы предоставляют интерфейс для взаимодействия с объектами, содержащими результат выполнения некоторой операции, время окончания которой неизвестно. Изначально любой promise не разрешен (unresolved) и будет разрешен либо с определенным значением (resolved), либо отвергнут с ошибкой (rejected). Как только promise становится разрешен или отвергнут, его состояние уже не может измениться, что обеспечивает неизменность состояния в течение какого угодно числа проверок. Что не означает, что на разных этапах проверок вы получите одно и тоже значение.
Кроме того, promise-ы можно объединять как для последовательного, так и для параллельного исполнения.
Далее все описание будет построено на базе AngularJS 1.1.5, а все примеры будут исполнены в виде тестов.
Итак, что из себя представляет promise? Это объект с двумя методами:
then(successCallback, errorCallback)
;always(callback)
;
Что в AngularJS вернет вам promise?
$http
— сервис для выполнения AJAX-запросов;$timeout
— AngularJS-обертка надsetTimeout
;- различные методы
$q
— сервиса для создания своих deferred-объектов и promise-ов.
Далее последовательно пройдемся по всем вариантам работы с promise-ами.
Простейшее использование
var responseData;
$http.get('http://api/user').then(function(response){
responseData = response.data;
});
Ничего интересного — callback и все. Но надо же от чего-то отталкиваться в изложении?.. :-)
Возврат значения из обработчиков
var User = function(data){
return angular.extend(this, data);
};
$httpBackend.expectGET('http://api/user').respond(200, {
result: {
data: [{name: 'Artem'}],
page: 1,
total: 10
}
});
var data = {};
$http.get('http://api/user').then(function(response){
var usersInfo = {};
usersInfo.list = _.collect(response.data.result.data, function(u){ return new User(u); });
usersInfo.total = response.data.result.total;
return usersInfo;
}).then(function(usersInfo){
data.users = usersInfo;
});
Благодаря такой цепочке
then
-ов можно строить многослойное приложение. ApiWrapper сделал запрос, выполнил общие обработчики ошибок, отдал данные из ответа без изменений на следующий слой. Там данные преобразовали нужным образом и отдали на следующий. И т.д.Любое возвращаемое значение из
then
придет в success
-callback
следующего then
. Не вернули ничего — в success-callback
придет undefined (см. тесты).Чтобы сделать
reject
— необходимо вернуть $q.reject(value)
. $httpBackend.expectGET('http://api/user').respond(400, {error_code: 11});
var error;
$http.get('http://api/user').then(
null,
function(response){
if (response.data && response.data.error_code == 10){
return {
list: [],
total: 0
};
}
return $q.reject(response.data ? response.data.error_code : null);
}
).then(
null,
function(errorCode){
error = errorCode;
}
);
Цепочки вызовов
$httpBackend.expectGET('http://api/user/10').respond(200, {id: 10, name: 'Artem', group_id: 1});
$httpBackend.expectGET('http://api/group/1').respond(200, {id: 1, name: 'Some group'});
var user;
$http.get('http://api/user/10').then(function(response){
user = response.data;
return $http.get('http://api/group/' + user.group_id);
}).then(function(response){
user.group = response.data;
});
Это позволит избежать пирамидального кода, сделать код более линейным, а значит более читабельным и простым в поддержке.
Параллельное выполнения запросов с ожиданием всех
$q.all(...)
принимает массив или словарь promise-объектов, объединяет их в один, который будет разрешен, когда разрешатся все promise, или отвергнут с ошибкой, когда хотя бы один promise будет отвергнут. При этом значения придут в success-callback либо в виде массива, либо в виде словаря в зависимости от того, как был вызван метод all
. $httpBackend.expectGET('http://api/obj1').respond(200, {type: 'obj1'})
var obj1, obj2;
var request1 = $http.get('http://api/obj1');
var request2 = $timeout(function(){ return {type: 'obj2'}; });
$q.all([request1, request2]).then(function(values){
obj1 = values[0].data;
obj2 = values[1];
});
expect(obj1).toBeUndefined();
expect(obj2).toBeUndefined();
$httpBackend.flush();
expect(obj1).toBeUndefined();
expect(obj2).toBeUndefined();
$timeout.flush();
expect(obj1).toEqual({type: 'obj1'});
expect(obj2).toEqual({type: 'obj2'});
$q.all({
obj1: $http.get('http://api/obj1'),
obj2: $timeout(function(){ return {type: 'obj2'}; })
}).then(function(values){
obj1 = values.obj1.data;
obj2 = values.obj2;
});
$q.when
Обернет любой объект в promise-объект. Чаще всего необходимо для моков в юнит-тестах. Или когда есть цепочка запросов, один из которых нужно выполнять не всегда, а только в некоторых случаях, а в остальных случаях есть уже готовый объект.
spyOn(UserApi, 'get').andReturn($q.when({id: 1, name: 'Artem'}));
var res;
UserApi.get(1).then(function(user){
res = user;
});
$rootScope.$digest();
expect(res).toEqual({id: 1, name: 'Artem'});
Обратите внимание, что для разрешения promise-ов должен быть выполнен хотя бы один
$digest
цикл.Создание своих deferred
-объектов
$q
-сервис также позволяет обернуть любую асинхронную операцию в свой deferred-объект с соответствующим ему promise-объектом. var postFile = function(name, file) {
var deferred = $q.defer();
var form = new FormData();
form.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + name, true);
xhr.onload = function(e) {
if (e.target.status == 200) {
deferred.resolve();
} else {
deferred.reject(e.target.status);
}
if (!$rootScope.$$phase) $rootScope.$apply();
};
xhr.send(form);
return deferred.promise;
};
Ключевые моменты здесь:
- создание своего deferred объекта:
var deferred = $q.defer()
- его разрешение в нужный момент:
deferred.resolve()
- или
reject
в случае ошибки:deferred.reject(e.target.status)
- возврат связанного promise-объекта:
return deferred.promise
Особенности AngularJS
Во-первых, $q-сервис реализован с учетом dirty-checking в AngularJS, что дает более быстрые resolve и reject, убирая ненужные перерисовки браузером.
Во-вторых, при интерполяции и вычислении angular-выражений promise-объекты интерпретируются как значение, полученное после resolve.
$rootScope.resPromise = $timeout(function(){ return 10; });
var res = $rootScope.$eval('resPromise + 2');
expect(res).toBe(2);
$timeout.flush();
res = $rootScope.$eval('resPromise + 2');
expect(res).toBe(12);
Это означает, что если следить за изменением переменной через строковый
watch
, то в качестве значения будет приходить именно resolved-значение. var res;
$rootScope.resPromise = $timeout(function(){ return 10; });
$rootScope.$watch('resPromise', function(newVal){
res = newVal;
});
expect(res).toBeUndefined();
$timeout.flush();
expect(res).toBe(10);
Исключение составляет использование функции. Возвращаемое из нее значение будет использовано как есть, т.е. это будет именно promise-объект.
$rootScope.resPromise = function(){
return $timeout(function(){ return 10; });
};
var res = $rootScope.$eval('resPromise()');
expect(typeof res.then).toBe('function');
Понимание этого важно при написании различных loading-виджетов, loading-директив для кнопок и т.п.
AngularJS 1.2.0 — что нового в promise-ах
catch(errorCallback)
как укороченный синоним дляpromise.then(null, errorCallback)
;- для большей схожести с Q
always(callback)
переименован вfinally(callback)
; - и самое существенное — поддержка нотификации в deferred- и promise-объектах, что удобно использовать для извещения о прогрессе и т.п. Теперь полная сигнатура
promise.then
выглядит какthen(successCallback, errorCallback, progressCallback)
, а уdeferred
появился методnotify(progress)
.
И напоследок скриншот выполненных тестов из WebStorm 7 EAP. Все же приятную добавили интеграцию с karma.
