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


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

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

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

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

25 789

25 ответов:

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

вы можете проверить, если $digest уже в ходе проверки $scope.$$phase.

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

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

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

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


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

угловое.js Anti Patterns

  1. не if (!$scope.$$phase) $scope.$apply(), это означает, что ваш $scope.$apply() недостаточно высоко в стеке вызовов.

из недавней дискуссии с угловатыми ребятами на эту самую тему:по причинам будущей проверки вы не должны использовать $$phase

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

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

Я недавно столкнулся с этим при написании угловых сервисов для обертывания 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, если оставить его незаданным ($timeout звонки $браузере.отложить, который по умолчанию 0, если задержка не установлено)

немного неинтуитивно, но это ответ от парней, пишущих угловые, так что это достаточно хорошо для меня!

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

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

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

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

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

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

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

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

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

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

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

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

  • $scope.$evalAsync(callback) был введен с angularjs 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);
});

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

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

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

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

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

удобный маленький вспомогательный метод, чтобы сохранить этот процесс сухим:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}

посмотреть http://docs.angularjs.org/error/метрики:инпрог

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

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

у меня была такая же проблема с сторонними скриптами, такими как CodeMirror например и Krpano, и даже с помощью safeApply методы, упомянутые здесь, не решили ошибку для меня.

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

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

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

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

этой

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

.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();

  }]
)

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

самая короткая сейф $apply - это:

$timeout(angular.noop)

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

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

иногда вы все равно получите ошибки, если вы используете этот способ (https://stackoverflow.com/a/12859093/801426).

попробуйте это:

if(! $rootScope.$root.$$phase) {
...

во-первых, не исправить это таким образом

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(); 
});

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

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

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

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

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

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

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


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

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);

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

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

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

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

$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.объем.$applyAsync() и область.$evalAsync () в AngularJS 1.3

  1. AngularJs Docs

понимание того, что угловые документы вызывают проверку $$phase an анти-шаблон, Я пытался сделать $timeout и _.defer на работу.

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

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

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

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

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

в CoffeeScript это еще красивее:

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

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

Это мой сервис 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']);
};

Я использую этот метод, и он, кажется, работает отлично. Это просто ждет окончания цикла, а затем запускает 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);
  }
}

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

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

можно использовать

$timeout

для предотвращения ошибки.

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

нашел это:https://coderwall.com/p/ngisma где Натан Уокер (в нижней части страницы) предлагает декоратор в $rootScope для создания функции '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;
      }
    ]);
  }
]);

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

if(!$scope.$$phase) {
  //TODO
}