Чому використовується if (! $ Scope. $$ phase) $ scope. $ Apply () анти-шаблон?


92

Іноді мені потрібно використовувати $scope.$applyв моєму коді, а іноді він видає помилку "Дайджест вже триває". Тож я почав знаходити спосіб обійти це питання і знайшов таке запитання: AngularJS: Запобігти помилці $ digest, яка вже виконується при виклику $ scope. $ Apply () . Однак у коментарях (і на кутовій вікі) ви можете прочитати:

Не робіть, якщо (! $ Scope. $$ phase) $ scope. $ Apply (), це означає, що ваш $ scope. $ Apply () недостатньо високий у стеку викликів.

Отже, зараз у мене є два запитання:

  1. Чому саме це анти-шаблон?
  2. Як я можу безпечно використовувати $ scope. $ Застосувати?

Ще одне "рішення" для запобігання помилці "дайджест вже триває", схоже, використовує $ timeout:

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

Це такий шлях? Чи безпечніше? Тож ось справжнє запитання: Як я можу повністю виключити можливість помилки "дайджест, який вже виконується"?

PS: Я використовую лише $ scope. $ Застосовується у зворотних викликах, які не є синхронними. (наскільки я знаю, це ситуації, коли ви повинні використовувати $ scope. $ apply, якщо ви хочете, щоб ваші зміни були застосовані)


З мого досвіду, ви завжди повинні знати, чи маніпулюєте ви scopeвсередині кута або ззовні кута. Тож відповідно до цього ви завжди знаєте, потрібно вам телефонувати scope.$applyчи ні. І якщо ви використовуєте один і той же код як для кутової, так і для некутової scopeманіпуляції, ви робите це неправильно, його слід завжди розділяти ... тому, в основному, якщо ви стикаєтесь із випадком, коли вам потрібно перевірити scope.$$phase, ваш код не є розроблений правильно, і завжди є спосіб зробити це «правильно»
doodeec

1
я використовую це лише для
некутових

2
якби він був digest already in progress
некутовим

1
це те, що я думав. Річ у тім, що це не завжди видає помилку. Лише раз у раз. Я підозрюю, що заявка зіткнулась ЗА ШАНСОМ із іншим дайджестом. Це можливо?
Домінік Голтерманн

Я не думаю, що це можливо, якщо зворотний дзвінок буде суто
некутовим

Відповіді:


113

Після ще кількох копань я зміг вирішити питання, чи завжди це безпечно використовувати $scope.$apply. Коротка відповідь - так.

Довга відповідь:

Через те, як ваш браузер виконує Javascript, неможливо випадково зіткнутися два дайджест-дзвінки .

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

Отже, помилка "дайджест, що вже виконується" може трапитися лише в одній ситуації: Коли заявка $ видається всередині іншої $ apply, наприклад:

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

Ця ситуація не може виникнути, якщо ми використовуємо $ scope.apply в чистому зворотному виклику, не пов'язаному з angularjs, як, наприклад, зворотний виклик setTimeout. Отже, наступний код на 100% захищений від кулі, і немає необхідності робити aif (!$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 }); });я справді не знаю, чому я повинен змушувати $ подавати заявку тут, тому що я використовую $ document.bind ..
Бетті Сент

оскільки $ document - це лише "обгортка jQuery або jqLite для об'єкта window.document браузера." і реалізовується наступним чином: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Там немає заявки.
Домінік Голтерманн

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 otherwies not та

Другим рішенням є використання $ timeout

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

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


1
проти; Питання конкретно задає питання, чому НЕ робити те, що ви тут описуєте, а не інший спосіб розірвати це. Дивіться чудову відповідь @gaul про те, коли використовувати $scope.$apply();.
PureSpider

Хоча не відповідаючи на питання: $timeoutце ключ! це працює, і пізніше я виявив, що це також рекомендується.
Хімел Наг Рана,

Я знаю, що пізно додати коментар до цього через 2 роки, але будьте обережні, використовуючи занадто багато часу очікування $, оскільки це може коштувати вам занадто багато продуктивності, якщо ви не маєте належної структури додатків
cpoDesign

9

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

А $digestперевірки циклу для об'єктів , тобто моделі (щоб бути точним $watch) , приєднані до $scopeоцінювати , якщо їх значення змінилися , і якщо він виявляє зміни , то він вживає необхідних заходів , щоб оновити уявлення.

Тепер, коли ви використовуєте, $scope.$applyви стикаєтеся з помилкою "Вже виконується", тому цілком очевидно, що працює дайджест $, але що його спричинило?

ans -> кожні $httpдзвінки, всі клацання, повтор, показ, приховування тощо запускають $digestцикл І НАЙГОРІША ЧАСТИНА, ЯКА ЗАБІГАЄ КОЖНОГО СФЕРИ ОБМЕЖЕННЯ.

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

Якщо у вас є 4 $scopeвластивості в кожному з них, то у вас на вашій сторінці в цілому 16 $ властивостей області.

Якщо ви вмикаєте $scope.$applyконтролер D, тоді $digestцикл перевірятиме всі 16 значень !!! плюс усі властивості $ rootScope.

Відповідь -> але $scope.$digestспрацьовує a $digestна дочірній і тій же області дії, тому він перевірить лише 4 властивості. Отже, якщо ви впевнені, що зміни D не вплинуть на A, B, C, тоді не використовуйте $scope.$digest $scope.$apply.

Отже, просто натискання клавіші ng-show / ng-show / hide може викликати $digestцикл для понад 100 властивостей, навіть коли користувач не запускав жодної події !


2
Так, я зрозумів це пізно в проекті, на жаль. Не використовував би Angular, якби я знав це з самого початку. Усі стандартні директиви запускають $ scope. $ Apply, який, у свою чергу, викликає $ rootScope. $ Digest, який виконує брудні перевірки ВСІХ областей. Невдале дизайнерське рішення, якщо ви запитаєте мене. Я мав би контролювати те, які обсяги слід перевіряти, оскільки Я ЗНАЮ, ЯК ДАНІ ПОСИЛАНІ ДО ЦЬОГО СФЕРИ ВЖИВАННЯ!
MoonStom

0

Використовуйте $timeout, це рекомендований спосіб.

Мій сценарій полягає в тому, що мені потрібно змінити елементи на сторінці на основі даних, отриманих від WebSocket. Оскільки поза межами Angular, без $ таймауту буде змінена єдина модель, але не вигляд. Оскільки Angular не знає, що частину даних було змінено. $timeoutв основному говорить Angular внести зміни в наступному раунді дайджесту $.

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

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

Набагато чистіше обернути код сокета в $ apply (подібно до того, як Angular застосовується до коду AJAX, тобто $http). В іншому випадку вам доведеться повторювати цей код всюди.
тимфруфлі

це точно не рекомендується. Крім того, ви іноді отримуватимете помилку під час цього, якщо $ scope має $$ фазу. натомість слід використовувати $ scope. $ evalAsync ();
FlavorScape

Не потрібно, $scope.$applyякщо ви використовуєте setTimeoutабо$timeout
Kunal

-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.