เหตุใดจึงใช้ if (! $ scope. $$ phase) $ scope. $ apply () an anti-pattern?


92

บางครั้งฉันจำเป็นต้องใช้$scope.$applyในโค้ดของฉันและบางครั้งก็แสดงข้อผิดพลาด "การสรุปข้อมูลที่กำลังดำเนินการอยู่" ดังนั้นผมจึงเริ่มที่จะหาทางรอบนี้และพบว่าคำถามนี้: AngularJS:. ป้องกันข้อผิดพลาด $ ย่อยแล้วในความคืบหน้าเมื่อโทร $ $ ขอบเขตใช้ () อย่างไรก็ตามในความคิดเห็น (และในวิกิเชิงมุม) คุณสามารถอ่าน:

อย่าทำถ้า (! $ scope. $$ phase) $ scope $ apply () หมายความว่า $ ขอบเขตของคุณ $ apply () ไม่สูงพอใน call stack

ตอนนี้ฉันมีสองคำถาม:

  1. เหตุใดจึงเป็นรูปแบบการต่อต้าน?
  2. ฉันจะใช้ $ scope อย่างปลอดภัย $ สมัครได้อย่างไร

"วิธีแก้ปัญหา" อื่นเพื่อป้องกันข้อผิดพลาด "ไดเจสต์กำลังดำเนินการอยู่" ดูเหมือนว่ากำลังใช้ $ timeout:

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

นั่นคือวิธีที่จะไป? ปลอดภัยกว่าไหม ดังนั้นนี่คือคำถามที่แท้จริง: ฉันจะสิ้นเชิงขจัดความเป็นไปได้ของข้อผิดพลาด "ย่อยแล้วในความคืบหน้า" หรือไม่?

PS: ฉันใช้ $ ขอบเขตเท่านั้น $ ใช้ในการเรียกกลับที่ไม่ใช่ angularjs ที่ไม่ซิงโครนัส (เท่าที่ฉันรู้ว่านั่นคือสถานการณ์ที่คุณต้องใช้ $ ขอบเขต $ ใช้ถ้าคุณต้องการนำการเปลี่ยนแปลงไปใช้)


จากประสบการณ์ของฉันคุณควรรู้เสมอว่าคุณกำลังจัดการscopeจากภายในเชิงมุมหรือจากภายนอกเชิงมุม ดังนั้นตามนี้คุณมักจะรู้ว่าคุณจำเป็นต้องโทรscope.$applyหรือไม่ และถ้าคุณใช้รหัสเดียวกันสำหรับการจัดการทั้งเชิงมุม / ไม่ใช่เชิงมุมแสดงscopeว่าคุณทำผิดก็ควรแยกออกจากกันเสมอ ... ดังนั้นโดยทั่วไปหากคุณพบกรณีที่คุณต้องตรวจสอบscope.$$phaseรหัสของคุณจะไม่ ออกแบบด้วยวิธีที่ถูกต้องและมีวิธีที่จะทำ 'วิธีที่ถูกต้อง' อยู่
เสมอ

1
ฉันใช้สิ่งนี้ในการเรียกกลับที่ไม่ใช่เชิงมุมเท่านั้น (!) นี่คือเหตุผลที่ฉันสับสน
Dominik Goltermann

2
ถ้ามันไม่ใช่เชิงมุมก็จะไม่เกิดdigest already in progressข้อผิดพลาด
doodeec

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 }); });ฉันไม่รู้จริงๆว่าทำไมฉันต้องสมัคร $ ที่นี่เพราะฉันใช้ $ document.bind ..
Betty St

เนื่องจาก $ document เป็นเพียง "กระดาษห่อ jQuery หรือ jqLite สำหรับออบเจ็กต์ window.document ของเบราว์เซอร์" และดำเนินการดังต่อไปนี้: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }ไม่มีการนำไปใช้ในนั้น
Dominik Goltermann

11
$timeoutในทางความหมายหมายถึงการรันโค้ดหลังจากเกิดความล่าช้า อาจเป็นสิ่งที่ปลอดภัยในการทำงาน แต่เป็นการแฮ็ก ควรมีวิธีที่ปลอดภัยในการใช้ $ apply เมื่อคุณไม่สามารถรู้ได้ว่า$digestรอบกำลังดำเนินอยู่หรือคุณอยู่ใน$applyไฟล์.
John Strickler

1
อีกสาเหตุหนึ่งที่ไม่ดี: ใช้ตัวแปรภายใน (เฟส $$) ซึ่งไม่ได้เป็นส่วนหนึ่งของ API สาธารณะและอาจมีการเปลี่ยนแปลงในเวอร์ชันใหม่กว่าของเชิงมุมและทำให้โค้ดของคุณเสียหาย ปัญหาของคุณเกี่ยวกับการเรียกเหตุการณ์ที่เป็นซิงโครนัสนั้นน่าสนใจ
Dominik Goltermann

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

หากเงื่อนไขข้างต้นเป็นจริงคุณสามารถใช้ $ ขอบเขตของคุณได้ $ ใช้ otherwies ไม่ใช่และ

วิธีที่สองคือใช้ $ timeout

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

มันจะไม่ปล่อยให้ส่วนย่อยอื่น ๆ เริ่มต้นจนกว่า $ timeout จะเสร็จสิ้นการดำเนินการ


1
โหวตลง; คำถามนี้ถามเป็นพิเศษว่าทำไมไม่ทำสิ่งที่คุณอธิบายไว้ที่นี่ไม่ใช่เพื่อแฮ็กข้อมูลอื่น ดูคำตอบที่ยอดเยี่ยมโดย @gaul ว่าควรใช้เมื่อ$scope.$apply();ใด
PureSpider

แม้ไม่ตอบคำถาม: $timeoutคือกุญแจสำคัญ! มันใช้งานได้และในภายหลังฉันพบว่ามันแนะนำด้วย
Himel Nag Rana

ฉันรู้ว่ามันค่อนข้างช้าที่จะเพิ่มความคิดเห็นใน 2 ปีต่อมา แต่โปรดระวังเมื่อใช้ $ timeout มากเกินไปเพราะอาจทำให้คุณเสียค่าใช้จ่ายในการทำงานมากเกินไปหากคุณไม่มีโครงสร้างแอปพลิเคชันที่ดี
cpoDesign

9

scope.$applyทริกเกอร์$digestวัฏจักรซึ่งเป็นพื้นฐานของการผูกข้อมูลแบบ 2 ทาง

$digestการตรวจสอบวงจรสำหรับวัตถุเช่นรุ่น (จะแม่นยำ$watch) ที่ติดอยู่กับ$scopeการประเมินว่าค่าของพวกเขามีการเปลี่ยนแปลงและหากตรวจพบการเปลี่ยนแปลงแล้วจะใช้เวลาในขั้นตอนที่จำเป็นในการปรับปรุงมุมมอง

ตอนนี้เมื่อคุณใช้ $scope.$applyคุณต้องเผชิญกับข้อผิดพลาด "อยู่ระหว่างดำเนินการ" ดังนั้นจึงค่อนข้างชัดเจนว่าการสรุป $ กำลังทำงานอยู่ แต่อะไรเป็นสาเหตุ

ans -> ทุกการ$httpโทร, คลิกทั้งหมด, ทำซ้ำ, แสดง, ซ่อนและอื่น ๆ จะทำให้เกิด$digestวงจรและส่วนที่แย่ที่สุดมันทำงานของทุกขอบเขต $

กล่าวคือหน้าของคุณมีตัวควบคุม 4 ตัวหรือคำสั่ง A, B, C, D

ถ้าคุณมี 4 $scopeคุณสมบัติในแต่ละคุณสมบัติคุณจะมีคุณสมบัติขอบเขต $ 16 ทั้งหมดในเพจของคุณ

หากคุณทริกเกอร์$scope.$applyในคอนโทรลเลอร์ D แล้ว a$digestวงจรจะตรวจสอบค่าทั้งหมด 16 ค่า !!! บวกคุณสมบัติ $ rootScope ทั้งหมด

คำตอบ ->แต่$scope.$digestทริกเกอร์$digestบนลูกและขอบเขตเดียวกันดังนั้นจะตรวจสอบคุณสมบัติ 4 อย่างเท่านั้น ดังนั้นหากคุณแน่ใจว่าการเปลี่ยนแปลงใน D จะไม่มีผลต่อ A, B, C ให้ใช้$scope.$diges$scope.$applyเสื้อไม่ได้

ดังนั้นเพียงแค่ ng-click หรือ ng-show / hide อาจทำให้เกิด$digestวงจรในคุณสมบัติมากกว่า 100+ แม้ว่าผู้ใช้จะไม่ได้เริ่มเหตุการณ์ใด ๆ ก็ตาม !


2
ใช่ฉันรู้ว่ามันล่าช้าในโครงการนี้ คงไม่ได้ใช้ Angular ถ้าฉันรู้เรื่องนี้ตั้งแต่แรก คำสั่งมาตรฐานทั้งหมดเริ่มใช้ $ ขอบเขต $ ใช้ซึ่งจะเรียก $ rootScope $ Digest ซึ่งจะทำการตรวจสอบสกปรกในทุกขอบเขต การตัดสินใจในการออกแบบไม่ดีถ้าคุณถามฉัน ฉันควรจะควบคุมขอบเขตที่ควรตรวจสอบสกปรกเพราะฉันรู้ว่าข้อมูลเชื่อมโยงกับขอบเขตเหล่านี้อย่างไร!
MoonStom

0

ใช้$timeoutเป็นวิธีที่แนะนำ

สถานการณ์ของฉันคือฉันต้องเปลี่ยนรายการบนเพจตามข้อมูลที่ฉันได้รับจาก WebSocket และเนื่องจากมันอยู่นอก Angular โดยไม่มี $ timeout โมเดลเดียวจะถูกเปลี่ยน แต่ไม่ใช่มุมมอง เนื่องจาก Angular ไม่ทราบว่ามีการเปลี่ยนแปลงข้อมูล$timeoutโดยพื้นฐานแล้วจะบอกให้ Angular ทำการเปลี่ยนแปลงในรอบถัดไปของ $ Dig

ฉันลองทำสิ่งต่อไปนี้แล้วและได้ผล ความแตกต่างสำหรับฉันคือ $ timeout นั้นชัดเจนกว่า

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

การห่อรหัสซ็อกเก็ตของคุณใน $ ใช้จะสะอาดกว่ามาก (เหมือนกับ Angular ในรหัส AJAX เช่น$http) มิฉะนั้นคุณจะต้องทำรหัสนี้ซ้ำทุกที่
timruffles

ไม่แนะนำอย่างแน่นอน นอกจากนี้บางครั้งคุณจะได้รับข้อผิดพลาดในการดำเนินการนี้หาก $ scope มี $$ เฟส คุณควรใช้ $ scope แทน $ evalAsync ();
FlavourScape

ไม่จำเป็น$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
        });
    }
])
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.