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

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

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

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

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

816
04 окт. '12 в 17:07
источник поделиться
25 ответов

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

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

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

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

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

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


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

angular.js Anti Patterns

  1. Не делайте, if (!$scope.$$phase) $scope.$apply(), это означает, что ваш $scope.$apply() недостаточно высок в стеке вызовов.
647
12 окт. '12 в 15:28
источник

Связанные вопросы


Похожие вопросы

Из недавнего обсуждения с ребятами 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, поэтому он достаточно хорош для меня!

653
25 сент. '13 в 7:06
источник

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

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

или если у вас есть lodash:

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

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

319
31 июля '13 в 1:51
источник

Многие из ответов здесь содержат хорошие советы, но также могут привести к путанице. Просто использование $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 )

263
16 апр. '14 в 9:59
источник

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

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
87
14 июня '13 в 21:14
источник

У меня была такая же проблема с сценариями сторонних разработчиков, как например 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();

  }]
)
32
03 сент. '13 в 3:15
источник

См. 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, так как это будет примерно то же самое.

32
22 янв. '14 в 0:33
источник

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

28
04 окт. '12 в 17:41
источник

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

$timeout(angular.noop)
14
13 нояб. '14 в 16:00
источник

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

scope.evalAsync(function(scope){
    //use the scope...
});
11
19 сент. '13 в 4:48
источник

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

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(); 
});
9
05 марта '18 в 11:25
источник

Иногда вы будете получать ошибки, если используете этот способ (qaru.site/questions/12065/...).

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

if(! $rootScope.$root.$$phase) {
...
9
30 апр. '13 в 16:24
источник

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

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

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

5
22 июля '14 в 15:05
источник

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

$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
4
12 марта '18 в 19:22
источник

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

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

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

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


$scope.$broadcast('customEventName', optionalCustomEventArguments);
4
02 янв. '14 в 18:26
источник

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);
3
21 нояб. '13 в 16:33
источник

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

2
21 мая '15 в 14:27
источник

Я смог решить эту проблему, вызвав $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 все, что вам нужно сделать.

2
02 сент. '13 в 0:20
источник

Это мой сервис 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
24 авг. '15 в 8:59
источник

Понимая, что документы 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
31 дек. '14 в 22:29
источник

Я использую этот метод, и он работает отлично. Это просто ждет завершения цикла, а затем вызывает 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
10 мая '16 в 17:23
источник

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

$timeout

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

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0
15 сент. '17 в 13:54
источник

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

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };
0
12 янв. '16 в 23:39
источник

Нашел это: 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;
      }
    ]);
  }
]);
-3
11 апр. '14 в 23:00
источник

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

if(!$scope.$$phase) {
  //TODO
}
-7
11 февр. '15 в 22:10
источник

Посмотрите другие вопросы по меткам или Задайте вопрос