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

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

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

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

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

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 идет, иногда нет. Вот почему я использую это проверять.

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


из комментариев: 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);
            }
        });
    });
});

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

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


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

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

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

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

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


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

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

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

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

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

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 / $rootScope:inprog

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


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

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

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

$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. Этот последний шаг вызывает ошибку " digest already in прогресс."

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

поэтому появляется что если digest будет работать в любом случае из-за какого-то другого изменения в Angular, $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 () против Scope.$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'

в чем проблема с этим? Есть ли альтернатива, которая не создаст FOUT? $ 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
}