Tại sao việc sử dụng if (! $ Scope. $$ phase) $ scope. $ Apply () là phản mẫu?


92

Đôi khi tôi cần sử dụng $scope.$applymã của mình và đôi khi nó xuất hiện lỗi "thông báo đã được xử lý". Vì vậy, tôi bắt đầu tìm cách giải quyết vấn đề này và tìm thấy câu hỏi này: AngularJS: Ngăn chặn lỗi $ thông báo đang diễn ra khi gọi $ scope. $ Apply () . Tuy nhiên, trong các nhận xét (và trên wiki góc cạnh), bạn có thể đọc:

Đừng làm nếu (! $ Scope. $$ phase) $ scope. $ Apply (), điều đó có nghĩa là $ scope. $ Apply () của bạn không đủ cao trong ngăn xếp cuộc gọi.

Vì vậy, bây giờ tôi có hai câu hỏi:

  1. Chính xác tại sao đây là một mô hình chống?
  2. Làm cách nào để tôi có thể sử dụng $ scope. $ Một cách an toàn?

Một "giải pháp" khác để ngăn lỗi "thông báo đã được xử lý" dường như đang sử dụng $ timeout:

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

đó là phải đường để đi không? Nó có an toàn hơn không? Vì vậy, đây là câu hỏi thực sự: Làm thế nào tôi có thể loại bỏ hoàn toàn khả năng xảy ra lỗi "thông báo đã được xử lý"?

Tái bút: Tôi chỉ đang sử dụng $ scope. $ Áp dụng trong các lệnh gọi lại non-anglejs không đồng bộ. (Theo như tôi biết, đó là những tình huống mà bạn phải sử dụng $ scope. $ áp dụng nếu bạn muốn các thay đổi của mình được áp dụng)


Theo kinh nghiệm của tôi, bạn nên luôn biết, nếu bạn đang thao tác scopetừ bên trong góc cạnh hoặc từ bên ngoài góc cạnh. Vì vậy, theo điều này bạn luôn biết, nếu bạn cần gọi scope.$applyhoặc không. Và nếu bạn đang sử dụng cùng một mã cho cả scopethao tác góc / không góc , bạn đang làm sai, nó phải luôn được tách biệt ... vì vậy về cơ bản nếu bạn gặp phải trường hợp cần kiểm tra scope.$$phase, mã của bạn không thiết kế theo một cách chính xác, và luôn luôn có một cách để làm điều đó 'đúng cách'
doodeec

1
Tôi đang sử dụng chỉ này trong callbacks phi góc Đây là lý do tại sao tôi đang bối rối (!)
Dominik Goltermann

2
nếu nó đã không góc cạnh, nó sẽ không ném digest already in progresslỗi
doodeec

1
đó là những gì tôi nghĩ. Vấn đề là: nó không phải lúc nào cũng ném ra lỗi. Chỉ thỉnh thoảng. Sự nghi ngờ của tôi là ứng dụng va chạm BẰNG CƠ HỘI với một thông báo khác. Điều đó có thể không?
Dominik Goltermann

Tôi không nghĩ rằng đó là có thể nếu gọi lại là đúng không góc cạnh
doodeec

Câu trả lời:


113

Sau một số lần đào sâu hơn, tôi đã có thể giải quyết câu hỏi liệu nó có luôn an toàn để sử dụng hay không $scope.$apply. Câu trả lời ngắn gọn là có.

Câu trả lời dài:

Do cách trình duyệt của bạn thực thi Javascript, không thể có hai lệnh gọi thông báo ngẫu nhiên va chạm .

Mã JavaScript mà chúng ta viết không phải tất cả đều chạy trong một lần, thay vào đó nó thực thi lần lượt. Mỗi lượt trong số này chạy liên tục từ đầu đến cuối và khi một lượt đang chạy, không có gì khác xảy ra trong trình duyệt của chúng tôi. (từ http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Do đó, lỗi "thông báo đã được xử lý" chỉ có thể xảy ra trong một trường hợp: Khi một $ apply được phát hành bên trong một $ apply khác, ví dụ:

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

Tình huống này không thể phát sinh nếu chúng ta sử dụng $ scope.apply trong một lệnh gọi lại thuần túy không có góc cạnhjs, chẳng hạn như lệnh gọi lại của setTimeout. Vì vậy, mã sau đây là chống đạn 100% và không cần phải làmif (!$scope.$$phase) $scope.$apply()

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

ngay cả cái này là an toàn:

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

Điều gì KHÔNG an toàn (vì $ timeout - giống như tất cả các trình trợ giúp của anglejs - đã gọi $scope.$applycho bạn):

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

Điều này cũng giải thích tại sao việc sử dụng if (!$scope.$$phase) $scope.$apply()là anti-pattern. Bạn chỉ đơn giản là không cần nó nếu bạn sử dụng $scope.$applyđúng cách: setTimeoutVí dụ như trong một lệnh gọi lại js thuần túy .

Đọc http://jimhoskins.com/2012/12/17/angularjs-and-apply.html để biết giải thích chi tiết hơn.


Tôi có một ví dụ trong đó tôi tạo một dịch vụ với $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });Tôi thực sự không biết tại sao tôi phải đặt $ áp dụng ở đây, vì tôi đang sử dụng $ document.bind ..
Betty St

bởi vì $ document chỉ là "Một trình bao bọc jQuery hoặc jqLite cho đối tượng window.document của trình duyệt." và được thực hiện như sau: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Không áp dụng trong đó.
Dominik Goltermann

11
$timeoutvề mặt ngữ nghĩa nghĩa là chạy mã sau một khoảng thời gian trì hoãn. Nó có thể là một điều an toàn về mặt chức năng để làm nhưng nó là một vụ hack. Nên có một cách an toàn để sử dụng $ apply khi bạn không thể biết liệu một $digestchu kỳ đang diễn ra hay bạn đã ở trong một chu kỳ $apply.
John Strickler

1
một lý do khác khiến nó tệ: nó sử dụng các biến nội bộ ($$ phase) không phải là một phần của api công khai và chúng có thể bị thay đổi trong một phiên bản mới hơn của angle và do đó làm hỏng mã của bạn. Vấn đề của bạn với sự kiện syncronous kích hoạt là mặc dù thú vị
Dominik Goltermann

4
Cách tiếp cận mới hơn là sử dụng $ scope. $ EvalAsync () thực thi an toàn trong chu kỳ thông báo hiện tại nếu có thể hoặc trong chu kỳ tiếp theo. Tham khảo bennadel.com/blog/…
jaymjarri

16

Nó chắc chắn là một mô hình chống bây giờ. Tôi đã thấy một thông báo phát nổ ngay cả khi bạn kiểm tra giai đoạn $$. Bạn chỉ không được phép truy cập API nội bộ được biểu thị bằng $$tiền tố.

Bạn nên sử dụng

 $scope.$evalAsync();

vì đây là phương pháp được ưa thích trong Angular ^ 1.4 và được hiển thị cụ thể như một API cho lớp ứng dụng.


9

Trong bất kỳ trường hợp nào khi thông báo của bạn đang được xử lý và bạn đẩy một dịch vụ khác để thông báo, nó chỉ báo lỗi tức là thông báo đã được xử lý. vì vậy để chữa khỏi điều này bạn có hai lựa chọn. bạn có thể kiểm tra bất kỳ thông báo nào khác đang diễn ra như bỏ phiếu.

Đầu tiên

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

nếu điều kiện trên là đúng, thì bạn có thể áp dụng phạm vi $ của mình. $ áp dụng các điều kiện khác không và

giải pháp thứ hai là sử dụng $ timeout

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

nó sẽ không để thông báo khác bắt đầu cho đến khi $ timeout hoàn tất việc thực thi nó.


1
không tán thành; Câu hỏi đặc biệt hỏi tại sao KHÔNG làm điều bạn đang mô tả ở đây, không phải cho một cách khác để hack xung quanh nó. Xem câu trả lời tuyệt vời của @gaul để biết khi nào sử dụng $scope.$apply();.
PureSpider

Mặc dù không trả lời câu hỏi: $timeoutlà chìa khóa! nó hoạt động và sau đó tôi thấy rằng nó cũng được khuyến khích.
Himel Nag Rana,

Tôi biết nó là khá muộn để thêm lời bình này 2 năm sau, nhưng phải cẩn thận khi sử dụng $ timeout quá nhiều, vì điều này có thể mất quá nhiều trong hoạt động nếu bạn không có cấu trúc ứng dụng tốt
cpoDesign

9

scope.$applygây ra một $digestchu kỳ mà là nền tảng cho dữ liệu 2 chiều ràng buộc

Một $digestchu trình kiểm tra các đối tượng tức là các mô hình (chính xác $watch) được gắn vào $scopeđể đánh giá xem giá trị của chúng có thay đổi hay không và nếu nó phát hiện ra sự thay đổi thì cần thực hiện các bước cần thiết để cập nhật chế độ xem.

Bây giờ khi sử dụng $scope.$applybạn gặp phải lỗi "Đã xảy ra" nên hiển nhiên là thông báo $ đang chạy nhưng điều gì đã kích hoạt nó?

ans -> mọi $httpcuộc gọi, tất cả ng-click, lặp lại, hiển thị, ẩn, v.v. đều kích hoạt một $digestchu kỳ VÀ PHẦN THỨ NHẤT NÓ CHẠY CỦA MỌI PHẠM VI $.

tức là trang của bạn có 4 bộ điều khiển hoặc chỉ thị A, B, C, D

Nếu bạn có 4 $scopethuộc tính trong mỗi thuộc tính thì bạn có tổng cộng 16 thuộc tính phạm vi trên trang của mình.

Nếu bạn kích hoạt $scope.$applytrong bộ điều khiển D thì một $digestchu kỳ sẽ kiểm tra tất cả 16 giá trị !!! cộng với tất cả các thuộc tính $ rootScope.

Trả lời -> nhưng $scope.$digestkích hoạt $digesttrên con và cùng một phạm vi nên nó sẽ chỉ kiểm tra 4 thuộc tính. Vì vậy, nếu bạn chắc chắn rằng những thay đổi trong D sẽ không ảnh hưởng đến A, B, C thì hãy sử dụng $scope.$digest not $scope.$apply.

Vì vậy, chỉ một ng-click hoặc ng-show / hide có thể kích hoạt một $digestchu kỳ trên hơn 100 thuộc tính ngay cả khi người dùng chưa kích hoạt bất kỳ sự kiện nào !


2
Vâng, thật không may, tôi đã nhận ra điều này muộn vào dự án. Sẽ không sử dụng Angular nếu tôi biết điều này ngay từ đầu. Tất cả các chỉ thị tiêu chuẩn kích hoạt một phạm vi $. $ Áp dụng, đến lượt nó gọi $ rootScope. $ Tiêu hóa, thực hiện kiểm tra bẩn trên TẤT CẢ các phạm vi. Quyết định thiết kế kém nếu bạn hỏi tôi. Tôi nên kiểm soát những phạm vi nào nên được kiểm tra bẩn, bởi vì tôi BIẾT CÁCH DỮ LIỆU LIÊN KẾT VỚI CÁC PHẠM VI NÀY!
MoonStom,

0

Sử dụng $timeout, đó là cách được khuyến khích.

Tình huống của tôi là tôi cần thay đổi các mục trên trang dựa trên dữ liệu tôi nhận được từ WebSocket. Và vì nó nằm ngoài Angular, không có $ timeout, mô hình duy nhất sẽ được thay đổi nhưng không thay đổi chế độ xem. Vì Angular không biết rằng phần dữ liệu đã bị thay đổi. $timeoutvề cơ bản là yêu cầu Angular thực hiện thay đổi trong vòng tiếp theo của $ tiêu hóa.

Tôi cũng đã thử cách sau và nó hoạt động. Sự khác biệt đối với tôi là $ timeout rõ ràng hơn.

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

Sẽ gọn gàng hơn nhiều khi quấn mã socket của bạn trong $ áp dụng (giống như Angular trên mã AJAX, tức là $http). Nếu không, bạn phải lặp lại mã này khắp nơi.
timruffles

điều này chắc chắn không được khuyến khích. Ngoài ra, đôi khi bạn sẽ gặp lỗi khi thực hiện việc này nếu $ scope có giai đoạn $$. thay vào đó, bạn nên sử dụng $ scope. $ evalAsync ();
FlavorScape

Không cần thiết $scope.$applynếu bạn đang sử dụng setTimeouthoặc$timeout
Kunal

-1

Tôi đã tìm thấy giải pháp rất hay:

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

tiêm vào nơi bạn cần:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.