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

    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+. Тем кому интересны эти отличия могут ознакомится с этой статьей.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 15
    • +2
      А как у $q с поддержкой нативных промисов?
      • +6
        На данный момент $q не поддерживает нативные промисы. Они только недавно попали в стандарт ES6, и поддерживаются далеко не всеми браузерами. Но в команде angular уже ведутся обсуждения на эту тему.
        • 0
          Насчёт нативных промисов вообще дебаты идут горячие. С одной стороны хорошо, с другой, такие высокоуровневые штуки в стандарте — это гарантия застоя. Например, насколько я знаю, текущая их реализация не проходит А+1.1
      • +6
        Главное не забывать, что кое-где ещё используемый IE8 использование ключевого слова catch как названия функции не переваривает:
        Вместо:
          return doSomethingAsync().catch(function(err) {  
            return errorHandler(err);
          });

        Надо:
         return doSomethingAsync()["catch"](function(err) {  
            return errorHandler(err);
          });
        • +5
          Да, вы правы. Я намеренно не написал об этом, дабы не усложнять примеры. Так же IE < 9 не переваривает finally, надо писать так:
          asyncFunction()["finally"](function () {
            importantFunction();
          })
          
        • +1
          Пятый пример — про меня, спасибо.
          • 0
            В п. 5 имеется в виду асинхронно? А если правда параллельно, то как?
            • 0
              Асинхронно конечно.
              • 0
                В том смысле, что в обработку запускаются все промисы. Это несколько увеличивает потребление памяти, но зато общий результат появляется быстрее. В обратной же ситуации (серийное выполнение) — функция будет ждать пока зарезолвится первый промис и затем приступит выполнять второй (представим, что функции в примере возвращают промис с кучей .then). Соответственно уменьшается потребление памяти, но и скорость немного падает. Это более критично в серверных реализациях промисов
              • 0
                asynkFunction

                Здесь и далее — явно опечатка.
              • 0
                Встречал ли кто-нибудь ресурсы типа «angular promise в картинках»? Мне приходится выстраивать довольно сложные цепочки с перехватом ошибок, ветвлениями, соединениями и заключениями, но получается это больше на интуитивном уровне, а хочется быть на 100% уверенным. Было бы здорово увидеть пример, в котором описаны различные звенья (с обработкой ошибок и без, множественными finally блоками если это возможно) и всеми возможными путями исполнения.
                • +1
                  А картинки Вам помогут быть уверенным? Unit-тесты тут должны лучше подойти…
                  • 0
                    Полностью согласен, но пишется намного быстрее и легче, когда знаешь и понимаешь, что там происходит.
                • +1
                  Забыли уточнить, что $q.when работает не только со значениями и ангуляровскими промисами, но с любыми другим промисами. Логика его работы такова, что если он находит в объекте метод then, то вытаскивает из него колбеки и заворачивает их в промис стандарта Promises/A+. Таким образом:

                  angularPromise = $q.when(jQueryPromise)

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.