Отличие $applyAsync от $evalAsync в Angular 1.3

Пост подготовлен на основе статьи “Scope.$applyAsync() vs. Scope.$evalAsync() in AngularJS 1.3” от Ben Nadel.

Из Angular1.2 мы все знаем метод скоупа $evalAsync, который позволяет вызывать код асинхронно (относительно цикла дайджеста) не прибегая к использованию сервиса $timeout.

В Angular1.3 был добавлен еще один метод – $applyAsync. После прочтения документации может оказаться, что по прежнему не ясно отличие этих 2х методов. С этим мы и попробуем разобраться в посте.

  • Оба метода обновляют глобальную очередь выражений ожидающих выполнения (но у каждого метода своя очередь: asyncQueue и applyAsyncQueue).
  • Оба метода выполняют выражение асинхронно, но $evalAsync() позволяет переопределить локальные зависимости(locals)
  • Оба метода выполняют выражение в try/catch блоке и отлавливают исключительные ситуации с помощью $exceptionHandler сервиса.

С первого взгляда методы идентичны. Различия становятся очевидными только, когда вы смотрите последовательность выполнения в цикле дайджеста: очередь $applyAsync – будет выполнена перед тем как Ангуляр начнет “грязную проверку”, то есть выполниться только один раз, а очередь $evalAsync будет выполнена перед каждым проходом цикла. Это означает что любое выражение добавленное в $evalAsync очередь во время цикла дайджеста будет выполнено на следующем шаге этого же цикла.

  • Оба метода выполняют свои очереди и после чего по таймеру запускают $digest на $rootScope.

И даже сейчас я не представляю зачем мы должны использовать новый метод $applyAsync. Вероятно для того, чтобы позволить отрисовывать DOM до выполнения выражения.

Чтобы получить больше ответов я погрузился в исходный код и обнаружил, что $applyAsync() метод используется самим фремворком – внутри $http сервиса, похоже $httpProvider теперь позволяет группировать ajax запросы идущие в интервале 10мс (вместо того, чтобы вызывать дайджест после каждого запроса)

Основываясь на исходном коде и выкинув все проверки я сделал что-то типа прототипа для обоих методов:

function $evalAsync(expr, locals) {
  $browser.defer($rootScope.$digest);
  asyncQueue.push({scope: this, expression: expr, locals: locals});
}

function $applyAsync(expr) {
  var scope = this;
  applyAsyncQueue.push(function(){
    scope.$eval(expr);
  });
  $browser.defer(function() {
    while (applyAsyncQueue.length) {
      applyAsyncQueue.shift()();
    }
    applyAsyncId = null;
  });
  $browser.defer($rootScope.$digest);
}

Откуда видно, что $applyAsync сам контроллирует свою очередь, а вот очередью $evalAsync занимается дайджест.

Вместо вывода могу сказать, что создание нового метода выглядит как микро-оптимизация, которая дает возможность фреймворку группировать ваши асинхронные выражения и выполнять их в рамках одного дайджеста.