Вот со всеми этими задачами могут помочь справиться 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
finally(callback)
переименован вfinally(callback)
; - и самое существенное — поддержка нотификации в deferred- и promise-объектах, что удобно использовать для извещения о прогрессе и т.п. Теперь полная сигнатура
promise.then
выглядит какthen(successCallback, errorCallback, progressCallback)
, а уdeferred
появился методnotify(progress)
.
И напоследок скриншот выполненных тестов из WebStorm 7 EAP. Все же приятную добавили интеграцию с karma.
ссылка на оригинал статьи http://habrahabr.ru/post/189084/
Добавить комментарий