Как стать автором
Обновить

Правильное использование promise в angular.js

Время на прочтение 5 мин
Количество просмотров 108K
imageВ процессе использования angular.js трудно обойтись без объекта $q (он же promise/deferred), ведь он лежит в основе всего фреймворка. Deferred механизм является очень простым и мощным инструментом, который позволяет писать лаконичный код. Но чтобы по-настоящему использовать эту мощь, необходимо знать обо всех возможностях данного инструмента.
Вот несколько моментов, о которых вы возможно не знали.




1. then всегда возвращает новый promise


Взглянем на пример:

function asyncFunction() {  
  var deferred = $q.defer();  
  doSomethingAsync().then(function(res) {  
    res = asyncManipulate(res);
    deferred.resolve(res);
  }, function(err) {
    deferred.reject(err);
  });

  return deferred.promise; 
}

Здесь бессмысленно создается новое обещание $q.defer(). Автор кода явно не знал, что then итак вернет promise. Чтобы улучшить код, просто вернем результат then:

function asyncFunction() { 
  return doSomethingAsync().then(function(res) {  
    return asyncManipulate(res);
  }); 
}


2. Результат promise «не теряется»


Снова пример:

function asyncFunction() {  
  return doSomethingAsync().then(function(res) {  
    return asyncManipulate(res);
  }, function(err) {
    return $q.reject(err);
  });
}

Любой результат выполнения функции doSomethingAsync, будь то resolve или reject, будет «всплывать» до тех пор пока не найдет свой обработчик (если обработчик вообще существует). Это значит, что если нет необходимости в обработке результата, то можно просто опустить соответствующий обработчик, ведь результат никуда не исчезнет, он просто пройдет дальше. В данном примере можно безболезненно убрать второй обработчик (обработка reject), так как никаких манипуляций не производится:

function asyncFunction() {  
  return doSomethingAsync().then(function(res) {  
    return asyncManipulate(res);
  });
}

Так же можно опустить обработку resolve, если нужно обработать только случай reject:

function asyncFunction() {  
  return doSomethingAsync().then(null, function(err) {  
    return errorHandler(err);
  });
}

Кстати, для такого случая существует синтаксический сахар:

function asyncFunction() {  
  return doSomethingAsync().catch(function(err) {  
    return errorHandler(err);
  });
}


3. Попасть в reject обработчик можно только вернув $q.reject()


Код:

asyncFunction().then(function (res) {
  // some code
  return res;
}, function (res) {
  // some code
}).then(function (res) {
  console.log('in resolve');
}, function (res) {
  console.log('in reject');
});

В данном примере, независимо от того как завершится функция asyncFunction, в консоли мы увидим 'in resolve'. Это происходит потому, что есть только один способ оказаться в reject обработчике — вернуть $q.reject(). В любых других случаях будет вызван resolve обработчик. Перепишем код так, чтобы видеть в консоли 'in reject', если asyncFunction вернет reject:

asyncFunction().then(function (res) {
  // some code
  return res;
}, function (res) {
  // some code
  return $q.reject(res);
}).then(function (res) {
  console.log('in resolve');
}, function (res) {
  console.log('in reject');
});


4. finally не меняет результат promise



asyncFunction().then(function (res) {
  importantFunction();
  return res;
}, function (err) {
  importantFunction();
  return $q.reject(err);
}).then(function (res) {
  // some resolve code
}, function (err) {
  // some reject code
})

Если нужно выполнить код независимо от результата promise, используют finally обработчик, который вызывается всегда. Так же блок finally не влияет на дальнейшую обработку, так как он не меняет тип promise результата. Улучшаем:

asyncFunction().finally(function () {
  importantFunction();
}).then(function (res) {
  // some resolve code
}, function (err) {
  // some reject code
})

Если finally обработчик вернет $q.reject(), то тогда следующим будет вызван reject обработчик. Способа гарантированно вызвать resolve обработчик нет.

5. $q.all выполняет функции параллельно


Рассмотрим вложенные цепочки вызовов:

loadSomeInfo().then(function(something) {  
  loadAnotherInfo().then(function(another) {
    doSomethingOnThem(something, another);
  });
});

Функции doSomethingOnThem требуется результат выполнения обеих функций loadSomeInfo и loadAnotherInfo. И не имеет значения в каком порядке они будут вызваны, важно лишь чтобы функция doSomethingOnThem была вызвана после того как получен результат от обеих функций. Значит, эти функции можно вызвать параллельно. Но автор данного кода явно не знал про $q.all метод. Перепишем:

$q.all([loadSomeInfo(), loadAnotherInfo()]).then(function (results) {
  doSomethingOnThem(results[0], results[1]);
});

$q.all принимает массив функций, которые будут запущены параллельно. Обещание, возвращаемое $q.all, будет вызвано, когда все функции в массиве завершатся. Результат будет доступен в виде массива results, в котором находятся результаты всех функций соответственно.
Таким образом, метод $q.all следует использовать в случаях, когда необходимо синхронизировать выполнение асинхронных функций.

6. $q.when превращает все в promise


Бывают ситуации, когда код может зависеть от асинхронной функции, а может зависеть от синхронной. И тогда вы создаете обертку над синхронной функцией, чтобы сохранить порядок в коде:

var promise;
if (isAsync){
  promise = asyncFunction();
} else {
  var localPromise = $q.defer(); 
  promise = localPromise.promise;
  localPromise.resolve(42);
}

promise.then(function (res) {
  // some code
});

В этом коде нет ничего плохого. Но есть способ сделать его чище:

$q.when(isAsync? asyncFunction(): 42).then(function (res) {
  // some code
});

$q.when своего рода прокси функция, которая принимает либо promise либо обычное значение, а возвращает всегда promise.

7. Правильная обработка ошибок в promise


Посмотрим на пример обработки ошибок в асинхронной функции:

function asyncFunction(){
  return $timeout(function meAsynk(){
    throw new Error('error in meAsynk');    
  }, 1);
}

try{
  asyncFunction();
} catch(err){
  errorHandler(err);
}

Вы видите здесь проблему? try/catch блок поймает только те ошибки, которые возникнут при выполнении функции asyncFunction. Но, после того как $timeout запустит свою callback функцию meAsynk, все ошибки которые там возникнут будут попадать в обработчик не перехваченных ошибок приложения (application’s uncaught exception handler). Соответственно, наш catch обработчик ничего не узнает.
Поэтому оборачивание асинхронных функций в try/catch бесполезно. Но что делать в таких ситуациях? Для этого асинхронные функции должны иметь специальный callback для обработки ошибок. В $q таким обработчиком является reject обработчик.
Переделаем код, чтобы ошибка оказалась в обработчике (используем описанный выше сахар catch):

function asyncFunction(){
  return $timeout(function meAsynk(){
    throw new Error('error in meAsynk');    
  }, 1);
}

asyncFunction().catch(function (err) {
  errorHandler(err);
});

Рассмотрим еще один пример:

function asyncFunction() {  
    var promise = doSomethingAsync();
    promise.then(function() {
        return somethingAsyncAgain();
    });

    return promise;
}

У этого кода есть одна проблема: если функция somethingAsyncAgain вернет reject (а как мы уже знаем reject вызывается и в случаях когда падают ошибки), то код, вызвавший нашу функцию никогда об этом не узнает. Обещания должны быть последовательными, каждое следующее должно зависеть от предыдущего. Но в данном примере обещание разорвано. Чтобы исправить перепишем так:

function asyncFunction() {  
    return doSomethingAsync().then(function() {
        return somethingAsyncAgain();
    });
}

Теперь код вызывающий нашу функцию полностью зависит от итога выполнения функции somethingAsyncAgain, и все ошибки могут быть обработаны вышестоящим кодом.

Посмотрим на этот пример:

asyncFunction().then(  
  function() {
    return somethingElseAsync();
  },
  function(err) {
    errorHandler(err);
});

Казалось бы, что на этот раз все правильно. Но если ошибка упадет в функции somethingElseAsync, то она не будет никем обработана. Перепишем код так, чтобы reject обработчик был обособлен:

asyncFunction().then(function() {
  return somethingElseAsync();
}).catch(function(err) {
  errorHandler(err);
});

Теперь любая возникающая ошибка будет обработана.

P.S.


Сервис $q является реализацией стандарта Promises/A+. Для более глубокого понимания рекомендую прочитать этот стандарт.
Так же стоит отметить, что реализация promise в jQuery отличается от стандарта Promises/A+. Тем кому интересны эти отличия могут ознакомится с этой статьей.
Теги:
Хабы:
+37
Комментарии 15
Комментарии Комментарии 15

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн