AngularJS: Предотвращение ошибки $ digest, которая уже выполняется при вызове $ scope. $ Apply ()

785

Я нахожу, что мне нужно обновлять мою страницу до области видимости вручную все больше и больше с момента создания приложения в angular.

Единственный способ, которым я знаю это сделать, - вызвать $apply() из области моих контроллеров и директив. Проблема заключается в том, что он продолжает бросать ошибку на консоль, которая читает:

Ошибка: $digest уже выполняется

Кто-нибудь знает, как избежать этой ошибки или добиться того же, но по-другому?

  • 32
    Это действительно разочаровывает, что нам нужно использовать $ apply все больше и больше.
  • 0
    Я также получаю эту ошибку, хотя я и звоню $ apply в обратном вызове. Я использую стороннюю библиотеку для доступа к данным на своих серверах, поэтому я не могу использовать преимущества $ http, да и не хочу, поскольку мне пришлось бы переписать их библиотеку для использования $ http.
Показать ещё 6 комментариев
Теги:
angularjs-scope
angular-digest

25 ответов

605
Лучший ответ

Не используйте этот шаблон. Это приведет к появлению большего количества ошибок, чем решает. Несмотря на то, что вы думаете, что он что-то исправил, это не так.

Вы можете проверить, выполняется ли $digest, проверив $scope.$$phase.

if(!$scope.$$phase) {
  //$digest or $apply
}

$scope.$$phase вернет "$digest" или "$apply", если выполняется $digest или $apply. Я считаю, что разница между этими состояниями заключается в том, что $digest будет обрабатывать часы текущего объема и его дочерних элементов, а $apply будет обрабатывать наблюдателей всех областей.

В точку @dnc253, если вы часто вызываете $digest или $apply, возможно, вы ошибаетесь. Обычно я нахожу, что мне нужно переварить, когда мне нужно обновить состояние области в результате запуска DOM-события вне досягаемости Angular. Например, когда твитер-бутстрап-модальный становится скрытым. Иногда событие DOM срабатывает, когда выполняется $digest, иногда нет. Вот почему я использую эту проверку.

Я хотел бы знать лучший способ, если кто-нибудь знает его.


Из комментариев: by @anddoutoi

angular.js Анти-шаблоны

  1. Не выполняйте if (!$scope.$$phase) $scope.$apply(), это означает, что ваш $scope.$apply() недостаточно высок в стеке вызовов.
  • 1
    Спасибо! Это спасло мой бекон. Я обрабатывал события из jQuery, которые запускались из элементов списка, в котором был фильтр. Я получил ошибку, когда редактировал содержимое одного из элементов в этом списке. Я много писал об этой проблеме и специально искал способ определить, выполняется ли дайджест $. Документы бесполезны для такого рода вещей.
  • 224
    Мне кажется, что $ digest / $ apply должен делать это по умолчанию
Показать ещё 21 комментарий
600

Из недавнего обсуждения с ребятами Angular по этой теме: Для будущих причин не следует использовать $$phase

При нажатии на "правильный" способ сделать это, ответ в настоящее время

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

Недавно я столкнулся с этим при написании сервисов Angular для обертывания API-интерфейсов facebook, google и twitter, которые в той или иной степени передавали обратные вызовы.

Вот пример из службы. (Для краткости остальная часть сервиса - настройка переменных, вложенных $timeout и т.д. - была остановлена.)

window.gapi.client.load('oauth2', 'v2', function() {
    var request = window.gapi.client.oauth2.userinfo.get();
    request.execute(function(response) {
        // This happens outside of angular land, so wrap it in a timeout 
        // with an implied apply and blammo, we're in action.
        $timeout(function() {
            if(typeof(response['error']) !== 'undefined'){
                // If the google api sent us an error, reject the promise.
                deferred.reject(response);
            }else{
                // Resolve the promise with the whole response if ok.
                deferred.resolve(response);
            }
        });
    });
});

Обратите внимание, что аргумент delay для $timeout не является обязательным и по умолчанию будет 0, если оставить unset ($timeout вызывает $browser.defer, который по умолчанию равен 0, если задержка не установлена ​​)

Немного неинтуитивно, но ответ от ребята, пишущего Angular, поэтому он достаточно хорош для меня!

  • 4
    Я сталкивался с этим много раз в моих директивах. Писал один для редактора, и это оказалось отлично работает. Я был на встрече с Брэдом Грином, и он сказал, что Angular 2.0 будет огромным без цикла дайджеста, используя встроенную возможность наблюдения JS и использование полифилла для браузеров, в которых его нет. На этом этапе нам больше не нужно это делать. :)
  • 0
    Вчера я видел проблему, когда вызов selectize.refreshItems () внутри $ timeout вызвал страшную ошибку рекурсивного дайджеста. Есть идеи, как это может быть?
Показать ещё 7 комментариев
297

Цикл дайджеста - это синхронный вызов. Это не приведет к управлению циклом событий браузера, пока он не будет выполнен. Есть несколько способов справиться с этим. Самый простой способ справиться с этим - использовать встроенный тайм-аут $, а второй - если вы используете подчеркивание или lodash (и вы должны быть), вызовите следующее:

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

или если у вас есть символ подчеркивания:

_.defer(function(){$scope.$apply();});

Мы попробовали несколько обходных решений, и нам не хотелось вводить $rootScope во все наши контроллеры, директивы и даже некоторые фабрики. Таким образом, $timeout и _.defer были нашими фаворитами до сих пор. Эти методы успешно сообщают angular ждать следующего цикла анимации, который гарантирует, что текущая область применения $apply закончена.

  • 4
    Это кажется интересным, почему это работает?
  • 1
    _.defer ожидает следующего цикла событий. К этому времени будет выполнен текущий дайджест, поэтому $ scope. $ Apply будет свободен для запуска, так как дайджест не будет запущен.
Показать ещё 17 комментариев
258

Многие из ответов здесь содержат хорошие советы, но также могут привести к путанице. Просто использование $timeout - не лучшее и правильное решение. Кроме того, обязательно прочтите, что, если вас беспокоит производительность или масштабируемость.

Вещи, которые вы должны знать

  • $$phase является приватным для фреймворка, и для этого есть веские причины.

  • $timeout(callback) будет ждать завершения текущего цикла дайджеста (если таковой имеется), затем выполнить обратный вызов, а затем запустить в конце полный $apply.

  • $timeout(callback, delay, false) сделает то же самое (с дополнительной задержкой перед выполнением обратного вызова), но не будет запускать $apply (третий аргумент), который сохраняет производительность, если вы не изменили модель Angular ($ масштаб).

  • $scope.$apply(callback) вызывает, помимо прочего, $rootScope.$digest, что означает, что он будет перенаправлять корневую область приложения и всех его дочерних элементов, даже если вы находитесь в изолированной области.

  • $scope.$digest() будет просто синхронизировать свою модель с представлением, но не будет переваривать область его родителей, что может сэкономить много результатов при работе над изолированной частью вашего HTML с изолированной областью (из директивы в основном). $digest не выполняет обратный вызов: вы выполняете код, затем перевариваете.

  • $scope.$evalAsync(callback) был введен с угловыми 1.2 и, вероятно, решит большинство ваших проблем. Пожалуйста, обратитесь к последнему абзацу, чтобы узнать больше об этом.

  • если вы получите $digest already in progress error, ваша архитектура неверна: вам не нужно перенастраивать вашу область действия, или вы не должны отвечать за нее (см. ниже).

Как структурировать код

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

function editModel() {
  $scope.someVar = someVal;
  /* Do not apply your scope here since we don't know if that
     function is called synchronously from Angular or from an
     asynchronous code */
}

// Processed by Angular, for instance called by a ng-click directive
$scope.applyModelSynchronously = function() {
  // No need to digest
  editModel();
}

// Any kind of asynchronous code, for instance a server request
callServer(function() {
  /* That code is not watched nor digested by Angular, thus we
     can safely $apply it */
  $scope.$apply(editModel);
});

И если вы знаете, что делаете, и работаете над изолированной небольшой директивой, входящей в большое приложение Angular, вы можете предпочесть $digest вместо $apply для сохранения производительности.

Обновление с Angularjs 1.2

В любой $scope добавлен новый мощный метод: $evalAsync. В принципе, он выполнит свой обратный вызов в текущем цикле дайджеста, если он произойдет, иначе новый цикл дайджеста начнет выполнение обратного вызова.

Это все еще не так хорошо, как $scope.$digest, если вы действительно знаете, что вам нужно только синхронизировать изолированную часть вашего HTML (поскольку новый $apply будет запущен, если ни один не выполняется), но это лучшее решение, когда вы выполняете функцию, которую вы не можете знать, если она будет выполняться синхронно или нет, например, после извлечения потенциально кэшированного ресурса: иногда для этого потребуется асинхронный вызов на сервер, иначе ресурс будет локально выбран синхронно.

В этих случаях и всех других, где у вас есть !$scope.$$phase, обязательно используйте $scope.$evalAsync( callback )

  • 3
    $timeout критикуется мимоходом. Можете ли вы дать больше причин, чтобы избежать $timeout ?
82

Удобный небольшой вспомогательный метод для поддержания этого процесса DRY:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
  • 6
    Ваше безопасное приложение помогло мне понять, что происходит намного больше, чем что-либо еще. Спасибо за публикацию этого.
  • 4
    Я собирался сделать то же самое, но разве это не значит, что есть шанс, что изменения, которые мы сделаем в fn (), не будут видны $ digest? Разве не было бы лучше отложить функцию, предполагая область действия. $$ phase === '$ digest'?
Показать ещё 4 комментария
32

См. http://docs.angularjs.org/error/$rootScope:inprog

Проблема возникает, если у вас есть вызов $apply, который иногда запускается асинхронно вне кода Angular (когда применяется $apply), а иногда синхронно внутри кода Angular (что вызывает ошибку $digest already in progress).

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

Способ предотвращения этой ошибки заключается в обеспечении того, чтобы код, вызывающий $apply, запускался асинхронно. Это можно сделать, выполнив код внутри вызова $timeout с задержкой, установленной на 0 (которая является значением по умолчанию). Однако вызов вашего кода внутри $timeout устраняет необходимость вызова $apply, потому что $timeout инициирует другой цикл $digest сам по себе, который, в свою очередь, сделает все необходимое обновление и т.д.

Решение

Короче говоря, вместо этого:

... your controller code...

$http.get('some/url', function(data){
    $scope.$apply(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

сделайте следующее:

... your controller code...

$http.get('some/url', function(data){
    $timeout(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

Вызов $apply только тогда, когда вы знаете, что запущенный код всегда будет выполняться вне кода Angular (например, ваш вызов $apply произойдет внутри обратного вызова, вызываемого кодом вне вашего кода Angular)..

Если кто-то не знает о каком-то значительном недостатке использования $timeout over $apply, я не понимаю, почему вы не всегда могли использовать $timeout (с нулевой задержкой) вместо $apply, так как это будет примерно то же самое.

  • 0
    Спасибо, это сработало для моего случая, когда я не вызываю $apply самостоятельно, но все равно получаю ошибку.
  • 5
    Основное отличие состоит в том, что $apply является синхронным (его обратный вызов выполняется, затем код, следующий за $ apply), а $timeout - нет: текущий код, следующий за тайм-аутом, выполняется, а новый стек начинается с обратного вызова, как если бы вы использовали setTimeout . Это может привести к графическим сбоям, если вы дважды обновляете одну и ту же модель: $timeout будет ждать обновления представления, прежде чем обновлять его снова.
Показать ещё 1 комментарий
31

У меня была такая же проблема с сценариями сторонних разработчиков, как например CodeMirror и Krpano, и даже используя методы safeApply, упомянутые здесь, не решили ошибку для меня.

Но для чего он решил использовать $timeout service (не забудьте сначала ввести его).

Таким образом, что-то вроде:

$timeout(function() {
  // run my code safely here
})

и если внутри вашего кода вы используете

это

возможно потому, что он внутри контроллера директивы factory или просто нуждается в каком-то привязке, тогда вы бы сделали что-то вроде:

.factory('myClass', [
  '$timeout',
  function($timeout) {

    var myClass = function() {};

    myClass.prototype.surprise = function() {
      // Do something suprising! :D
    };

    myClass.prototype.beAmazing = function() {
      // Here 'this' referes to the current instance of myClass

      $timeout(angular.bind(this, function() {
          // Run my code safely here and this is not undefined but
          // the same as outside of this anonymous function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)
27

Когда вы получаете эту ошибку, это в основном означает, что она уже находится в процессе обновления вашего представления. Вам действительно не нужно вызывать $apply() в вашем контроллере. Если ваше представление не обновляется, как вы ожидали, а затем вы получите эту ошибку после вызова $apply(), это скорее всего означает, что вы не обновляете модель правильно. Если вы опубликуете некоторые особенности, мы можем выяснить основную проблему.

  • 0
    хех, я потратил целый день на то, чтобы выяснить, что AngularJS просто не может смотреть привязки "волшебным образом", и я должен иногда нажимать на него с помощью $ apply ().
  • 0
    что вообще означает, you're not updating the the model correctly ? $scope.err_message = 'err message'; не правильное обновление?
Показать ещё 2 комментария
14

Самая короткая форма безопасного $apply:

$timeout(angular.noop)
11

Вы также можете использовать evalAsync. Он будет запущен через некоторое время после завершения дайджеста!

scope.evalAsync(function(scope){
    //use the scope...
});
9

Иногда вы будете получать ошибки, если используете этот способ (https://stackoverflow.com/questions/12729122/angularjs-prevent-error-digest-already-in-progress-when-calling-scope-apply).

Попробуйте следующее:

if(! $rootScope.$root.$$phase) {
...
  • 5
    использование обоих! $ scope. $$ phase и! $ scope. $ root. $$ phase (not! $ rootScope. $ root. $$ phase) работает для меня. +1
  • 2
    $rootScope и anyScope.$root - это один и тот же парень. $rootScope.$root является избыточным.
6

Прежде всего, не исправляйте это так

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}

Это не имеет смысла, потому что $ phase - это просто логический флаг цикла $ digest, поэтому ваш $ apply() иногда не запускается. И помните, что это плохая практика.

Вместо этого используйте $timeout

    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});

Если вы используете подчеркивание или lodash, вы можете использовать defer():

_.defer(function(){ 
  $scope.$apply(); 
});
5

Вы должны использовать $evalAsync или $timeout в соответствии с контекстом.

Это ссылка с хорошим объяснением:

http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm

4

Я бы посоветовал вам использовать настраиваемое событие, а не запускать цикл дайджеста.

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

Создавая настраиваемое событие, вы также более эффективно выполняете свой код, потому что вы вызываете только прослушиватели, подписавшиеся на указанное событие, и НЕ запускаете все часы, привязанные к области, как если бы вы вызывали область. $apply.

$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);
3

yearofmoo проделал отличную работу по созданию для нас функции повторного использования $safeApply:

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

Использование:

//use by itself
$scope.$safeApply();

//tell it which scope to update
$scope.$safeApply($scope);
$scope.$safeApply($anotherScope);

//pass in an update function that gets called when the digest is going on...
$scope.$safeApply(function() {

});

//pass in both a scope and a function
$scope.$safeApply($anotherScope,function() {

});

//call it on the rootScope
$rootScope.$safeApply();
$rootScope.$safeApply($rootScope);
$rootScope.$safeApply($scope);
$rootScope.$safeApply($scope, fn);
$rootScope.$safeApply(fn);
2

попробуйте использовать

$scope.applyAsync(function() {
    // your code
});

вместо

if(!$scope.$$phase) {
  //$digest or $apply
}

$ applyAsync Запланируйте вызов $ apply, чтобы произойти в более позднее время. Это можно использовать для очереди нескольких выражений, которые необходимо оценивать в одном и том же дайджесте.

ПРИМЕЧАНИЕ. В $ digest $ applyAsync() будет только скрываться, если текущая область действия - это $ rootScope. Это означает, что если вы вызовете $ digest в дочерней области, он не будет явно скрывать очередь $ applyAsync().

Exmaple:

  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

Рекомендации:

1. Scope. $ ApplyAsync() vs. Scope. $ EvalAsync() в AngularJS 1.3

  1. AngularJs Docs
2

используйте $scope.$$phase || $scope.$apply(); вместо

2

Я смог решить эту проблему, вызвав $eval вместо $apply в местах, где я знаю, что будет выполняться функция $digest.

В соответствии с docs, $apply в основном делает следующее:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

В моем случае, ng-click изменяет переменную в пределах области действия, а $watch на этой переменной меняет другие переменные, которые должны быть $applied. Этот последний шаг приводит к тому, что ошибка "дайджест уже выполняется".

Заменив $apply на $eval внутри выражения watch, переменные области обновляются, как ожидалось.

Следовательно, кажется, что если дайджест будет работать в любом случае из-за какого-либо другого изменения внутри Angular, $eval 'ing все, что вам нужно сделать.

1

Я использую этот метод, и он работает отлично. Это просто ждет завершения цикла, а затем вызывает apply(). Просто вызовите функцию apply(<your scope>) из любой точки.

function apply(scope) {
  if (!scope.$$phase && !scope.$root.$$phase) {
    scope.$apply();
    console.log("Scope Apply Done !!");
  } 
  else {
    console.log("Scheduling Apply after 200ms digest cycle already in progress");
    setTimeout(function() {
        apply(scope)
    }, 200);
  }
}
1

Это мой сервис utils:

angular.module('myApp', []).service('Utils', function Utils($timeout) {
    var Super = this;

    this.doWhenReady = function(scope, callback, args) {
        if(!scope.$$phase) {
            if (args instanceof Array)
                callback.apply(scope, Array.prototype.slice.call(args))
            else
                callback();
        }
        else {
            $timeout(function() {
                Super.doWhenReady(scope, callback, args);
            }, 250);
        }
    };
});

и это пример использования:

angular.module('myApp').controller('MyCtrl', function ($scope, Utils) {
    $scope.foo = function() {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.foo);

    $scope.fooWithParams = function(p1, p2) {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.fooWithParams, ['value1', 'value2']);
};
1

Понимая, что документы Angular вызывают проверку $$phase a anti-pattern, я попытался получить $timeout и _.defer для работы.

Тайм-аут и отложенные методы создают вспышку нераспакованного содержимого {{myVar}} в dom, как FOUT. Для меня это было неприемлемо. Это оставляет меня без особого догматического подтверждения, что что-то является взломом и не имеет подходящей альтернативы.

Единственное, что работает каждый раз:

if(scope.$$phase !== '$digest'){ scope.$digest() }.

Я не понимаю опасности этого метода или почему он описывается как взлом людей в комментариях и команде Angular. Команда кажется точной и легкой для чтения:

"Сделайте дайджест, если он уже не происходит"

В CoffeeScript он еще красивее:

scope.$digest() unless scope.$$phase is '$digest'

В чем проблема? Есть ли альтернатива, которая не создаст FOUT? $safeApply выглядит отлично, но также использует метод проверки $$phase.

  • 1
    Я хотел бы видеть информированный ответ на этот вопрос!
  • 0
    Это хак, потому что это означает, что вы упускаете контекст или не понимаете код в этой точке: либо вы находитесь в цикле углового дайджеста и вам это не нужно, либо вы асинхронно вне этого, а затем вам это нужно. Если вы не можете знать, что в этом пункте кода, то вы не несете ответственности за его усвоение
0

Вы можете использовать

$timeout

чтобы предотвратить ошибку.

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0

похоже на ответы выше, но это верно сработало для меня... в службе добавьте:

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };
-3

Нашел это: https://coderwall.com/p/ngisma, где Натан Уокер (около нижней части страницы) предлагает декоратору в $rootScope создать func 'safeApply', код:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);
-8

Это решит вашу проблему:

if(!$scope.$$phase) {
  //TODO
}
  • 0
    Не делайте, если (! $ Scope. $$ phase) $ scope. $ Apply (), это означает, что ваш $ scope. $ Apply () недостаточно высок в стеке вызовов.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню