it-swarm.com.ru

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

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

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

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

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

819
Lightbulb1

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

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

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

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

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

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


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

angular.js Anti Patterns

  1. Не выполняйте if (!$scope.$$phase) $scope.$apply(), это означает, что ваша $scope.$apply() недостаточно высока в стеке вызовов.
648
Lee

Из недавней дискуссии с парнями 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);
            }
        });
    });
});

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

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

653
betaorbust

Цикл дайджеста - это синхронный вызов. Это не даст контроль над циклом событий браузера, пока это не будет сделано. Есть несколько способов справиться с этим. Самый простой способ справиться с этим - использовать встроенный тайм-аут в $ 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.

321
frosty

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

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

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

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

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

  • $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);
});

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

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

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

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

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

264
floribon

Удобный маленький вспомогательный метод для сохранения этого процесса

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

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

32
Trevor

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

  }]
)
32
Ciul

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

28
dnc253

Кратчайшая форма безопасного $apply:

$timeout(angular.noop)
14
Warlock

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

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

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

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

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

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

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

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

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

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

5
Luc

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

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

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

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


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

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

$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 () против Scope. $ EvalAsync () в AngularJS 1.

  1. AngularJs Docs
4
Eduardo Eljaiek

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
RNobel

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

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

2
teleclimber

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

2
Visakh B Sujathan

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

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

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

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

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

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

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

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

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

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

1
SimplGy

Это мой сервис утилит:

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
ranbuch

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

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

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

$timeout

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

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