Почему используется if (! $ Scope. $$ phase) $ scope. $ Apply () как антипаттерн?


92

Иногда мне нужно использовать $scope.$applyв своем коде, а иногда возникает ошибка «дайджест уже выполняется». Итак, я начал искать способ обойти это и нашел следующий вопрос: AngularJS: Предотвратить уже выполняющийся дайджест ошибки при вызове $ scope. $ Apply () . Однако в комментариях (и на вики-странице angular) вы можете прочитать:

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

Итак, теперь у меня есть два вопроса:

  1. Почему именно это антипаттерн?
  2. Как я могу безопасно использовать $ scope. $ Apply?

Другое "решение" для предотвращения ошибки "дайджест уже выполняется", похоже, использует $ timeout:

$timeout(function() {
  //...
});

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

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


По моему опыту, вы всегда должны знать, управляете ли вы scopeиз angular изнутри или извне angular. Так что по этому вы всегда знаете, нужно вам звонить scope.$applyили нет. И если вы используете один и тот же код для угловых и неугловых scopeманипуляций, вы делаете это неправильно, он всегда должен быть разделен ... поэтому в основном, если вы столкнетесь с ситуацией, когда вам нужно проверить scope.$$phase, ваш код не спроектирован правильно, и всегда есть способ сделать это «правильно»
doodeec

1
Я использую это только в обратных вызовах, отличных от angular (!) Вот почему я запутался
Доминик Гольтерманн

2
если бы он был не угловым, он бы не digest already in progress
выдал

1
это то, о чем я думал. Дело в том, что он не всегда вызывает ошибку. Только изредка. Я подозреваю, что приложение СЛУЧАЙНО сталкивается с другим дайджестом. Это возможно?
Dominik Goltermann

Я не думаю, что это возможно, если обратный вызов строго не угловой
doodeec

Ответы:


113

Еще немного покопавшись, я смог решить вопрос, всегда ли его безопасно использовать $scope.$apply. Краткий ответ: да.

Длинный ответ:

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

Код JavaScript, который мы пишем, не выполняется за один раз, а выполняется по очереди. Каждый из этих ходов выполняется без перерывов от начала до конца, и когда выполняется поворот, в нашем браузере больше ничего не происходит. (с http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Следовательно, ошибка «дайджест уже выполняется» может возникнуть только в одной ситуации: когда $ apply выдается внутри другого $ apply, например:

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

Эта ситуация не может возникнуть, если мы используем $ scope.apply в чистом обратном вызове, отличном от angularjs, как, например, обратный вызов setTimeout. Таким образом, следующий код на 100% надежен, и нет необходимости делатьif (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

даже этот безопасен:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

Что НЕ безопасно (потому что $ timeout - как и все помощники angularjs - уже вызывает $scope.$applyвас):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

Это также объясняет, почему использование if (!$scope.$$phase) $scope.$apply()является анти-шаблоном. Он вам просто не нужен, если вы используете $scope.$applyего правильно: например, в чистом обратном вызове js setTimeout.

Прочтите http://jimhoskins.com/2012/12/17/angularjs-and-apply.html для более подробного объяснения.


У меня есть пример, в котором я создаю службу, и $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });я действительно не знаю, почему я должен использовать здесь $ apply, потому что я использую $ document.bind ..
Бетти Стрит,

потому что $ document - это всего лишь «оболочка jQuery или jqLite для объекта window.document браузера». и реализовано следующим образом: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }там нет заявки.
Dominik Goltermann

11
$timeoutсемантически означает запуск кода после задержки. Это может быть функционально безопасно, но это взлом. Должен быть безопасный способ использования $ apply, когда вы не можете узнать, выполняется ли $digestцикл или вы уже находитесь внутри $apply.
Джон Стриклер

1
еще одна причина, почему это плохо: он использует внутренние переменные (фаза $$), которые не являются частью общедоступного API, и они могут быть изменены в более новой версии angular и, таким образом, нарушают ваш код. Однако интересна ваша проблема с синхронным запуском событий
Доминик Гольтерманн

4
Более новый подход заключается в использовании $ scope. $ EvalAsync (), который безопасно выполняется в текущем цикле дайджеста, если это возможно, или в следующем цикле. См. Bennadel.com/blog/…
jaymjarri

16

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

Вы должны использовать

 $scope.$evalAsync();

так как это предпочтительный метод в Angular ^ 1.4 и специально представлен как API для уровня приложения.


9

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

Первый

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

если вышеупомянутое условие истинно, то вы можете применить свою $ scope. $ apply в противном случае и

Второе решение - использовать $ timeout

$timeout(function() {
  //...
})

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


1
проголосовали против; В вопросе конкретно спрашивается, почему НЕ делать то, что вы здесь описываете, а не о другом способе обойти это. Смотрите отличный ответ @gaul, когда использовать $scope.$apply();.
PureSpider

Хотя и не отвечая на вопрос: $timeoutэто ключ! это работает, и позже я обнаружил, что это тоже рекомендуется.
Himel Nag Rana

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

9

scope.$applyзапускает $digestцикл, который является фундаментальным для двусторонней привязки данных

А $digestпроверки цикла для объектов , то есть модели (чтобы быть точным $watch) , присоединенные к $scopeоценивать , если их значения изменились , и если он обнаруживает изменения , то он предпринимает необходимые шаги , чтобы обновить представление.

Теперь, когда вы используете, $scope.$applyвы сталкиваетесь с ошибкой «Уже выполняется», поэтому совершенно очевидно, что $ дайджест запущен, но что его вызвало?

ans -> каждый $httpвызов, все ng-click, повторение, отображение, скрытие и т. д. запускают $digestцикл, И НАХУДШАЯ ЧАСТЬ ЗАПУСКАЕТ КАЖДЫЙ $ SCOPE.

т.е. скажем, на вашей странице 4 контроллера или директивы A, B, C, D

Если у вас есть 4 $scopeсвойства в каждом из них, то на вашей странице будет всего 16 свойств области действия.

Если вы запускаете $scope.$applyконтроллер D, $digestцикл проверяет все 16 значений !!! плюс все свойства $ rootScope.

Ответ -> но $scope.$digestзапускает $digestдочернюю и ту же область видимости, поэтому проверяет только 4 свойства. Поэтому, если вы уверены, что изменения в D не повлияют на A, B, C, тогда используйте $scope.$digest not $scope.$apply.

Таким образом, простой ng-click или ng-show / hide может запустить $digestцикл для более чем 100+ свойств, даже если пользователь не инициировал никакого события !


2
Да, к сожалению, я понял это поздно в проекте. Я бы не использовал Angular, если бы знал это с самого начала. Все стандартные директивы запускают $ scope. $ Apply, который, в свою очередь, вызывает $ rootScope. $ Digest, который выполняет грязные проверки для ВСЕХ областей. Плохое дизайнерское решение, если вы спросите меня. Я должен контролировать, какие области следует проверять, потому что Я ЗНАЮ, КАК ДАННЫЕ СВЯЗАНЫ С ЭТИМИ ОБЛАСТЯМИ!
MoonStom

0

Используйте $timeoutрекомендуемый способ.

Мой сценарий заключается в том, что мне нужно изменить элементы на странице на основе данных, полученных от WebSocket. И поскольку он находится за пределами Angular, без тайм-аута $ будет изменена единственная модель, но не представление. Потому что Angular не знает, что часть данных была изменена. $timeoutв основном говорит Angular внести изменения в следующий раунд $ digest.

Я тоже пробовал следующее, и это работает. Для меня разница в том, что таймаут $ более понятен.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)

Намного проще обернуть код сокета в $ apply (как и Angular в коде AJAX, т.е. $http). В противном случае вам придется повторять этот код повсюду.
Timruffles

это определенно не рекомендуется. Кроме того, при этом иногда возникает ошибка, если в $ scope есть фаза $$. вместо этого вы должны использовать $ scope. $ evalAsync ();
FlavorScape

Нет необходимости $scope.$applyиспользовать setTimeoutили$timeout
Кунал

-1

Нашел очень крутое решение:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

введите это там, где вам нужно:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.