AngularJS: ป้องกันข้อผิดพลาดในการแยกย่อย $ ที่กำลังดำเนินอยู่เมื่อมีการโทรขอบเขต $ $ ใช้ ()


838

ฉันพบว่าฉันต้องอัปเดตหน้าเว็บของฉันเป็นขอบเขตด้วยตนเองมากขึ้นเรื่อย ๆ นับตั้งแต่สร้างแอปพลิเคชันเป็นมุม

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

ข้อผิดพลาด: กำลังสรุป $ กำลังดำเนินการอยู่

ไม่มีใครรู้วิธีหลีกเลี่ยงข้อผิดพลาดนี้หรือทำสิ่งเดียวกัน แต่แตกต่างกัน?


34
มันเป็นเรื่องน่าผิดหวังจริง ๆ ที่เราต้องการใช้ $ ใช้มากขึ้นเรื่อย ๆ
OZ_

ฉันได้รับข้อผิดพลาดนี้เช่นกันแม้ว่าฉันจะโทรไปที่ $ จะใช้ในการโทรกลับ ฉันใช้ห้องสมุดบุคคลที่สามเพื่อเข้าถึงข้อมูลบนเซิร์ฟเวอร์ของพวกเขาดังนั้นฉันจึงไม่สามารถใช้ประโยชน์จาก $ http หรือฉันไม่ต้องการเพราะฉันจะต้องเขียนห้องสมุดของพวกเขาอีกครั้งเพื่อใช้ $ http
เทรเวอร์

45
การใช้งาน$timeout()
Onur Yıldırım

6
ใช้ $ timeout (fn) + 1, มันสามารถแก้ไขปัญหา,! $ scope. $$ phase ไม่ใช่ทางออกที่ดีที่สุด
Huei Tan

1
ตัดโค้ด / ขอบเขตการโทรเท่านั้น $ ใช้จากภายในหมดเวลา (ไม่ใช่ $ หมดเวลา) ฟังก์ชั่น AJAX (ไม่ใช่ $ http) และกิจกรรม (ไม่ng-*) ตรวจสอบให้แน่ใจถ้าคุณจะเรียกมันว่าจากภายในฟังก์ชั่น (ที่เรียกว่าผ่านหมดเวลา / อาแจ็กซ์ / เหตุการณ์) ว่ามันไม่ได้นอกจากนี้ยังถูกเรียกใช้ในการโหลดแรก
Patrick

คำตอบ:


660

อย่าใช้รูปแบบนี้ - สิ่งนี้จะทำให้เกิดข้อผิดพลาดมากกว่าที่จะแก้ไขได้ แม้ว่าคุณคิดว่าจะแก้ไขบางอย่าง แต่ก็ไม่ได้

คุณสามารถตรวจสอบว่ามีอยู่แล้วในความคืบหน้าการตรวจสอบโดย$digest$scope.$$phase

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

$scope.$$phaseจะส่งคืน"$digest"หรือ"$apply"ถ้า a $digestหรือ$applyกำลังดำเนินการอยู่ ฉันเชื่อว่าความแตกต่างระหว่างรัฐเหล่านี้คือการที่$digestจะประมวลผลนาฬิกาของขอบเขตปัจจุบันและลูก ๆ ของมันและ$applyจะประมวลผลผู้เฝ้าดูของขอบเขตทั้งหมด

ถึงจุดของ @ dnc253 หากคุณพบว่าตัวเองโทรมา$digestหรือ$applyบ่อยครั้งคุณอาจทำผิด ฉันมักจะพบว่าฉันจำเป็นต้องแยกแยะเมื่อฉันต้องการอัปเดตสถานะของขอบเขตเนื่องจากเหตุการณ์ DOM ที่เริ่มทำงานนอกขอบเขตของ Angular ตัวอย่างเช่นเมื่อ modal bootstrap twitter ถูกซ่อน บางครั้งเหตุการณ์ DOM เริ่มเมื่อ a $digestอยู่ระหว่างดำเนินการ นั่นเป็นเหตุผลที่ฉันใช้เช็คนี้

ฉันชอบที่จะรู้วิธีที่ดีกว่าถ้าใครรู้


จากความคิดเห็น: โดย @anddoutoi

รูปแบบการต่อต้าน angular.js

  1. อย่าทำif (!$scope.$$phase) $scope.$apply()มันหมายความว่าคุณ$scope.$apply()ยังไม่สูงพอในสแต็กการโทร

230
ดูเหมือนว่าฉันชอบ $ digest / $ Apply ควรทำสิ่งนี้เป็นค่าเริ่มต้น
Roy Truelove

21
โปรดทราบว่าในบางกรณีฉันต้องตรวจสอบ แต่ขอบเขตปัจจุบันและขอบเขตรูท ฉันได้รับค่าสำหรับ $$ phase บนรูท แต่ไม่อยู่ในขอบเขตของฉัน คิดว่ามันเกี่ยวข้องกับขอบเขตของคำสั่ง แต่ ..
รอย Truelove

106
"หยุดทำif (!$scope.$$phase) $scope.$apply()", github.com/angular/angular.js/wiki/Anti-Patterns
anddoutoi

34
@anddoutoi: เห็นด้วย; ลิงก์ของคุณทำให้ชัดเจนว่านี่ไม่ใช่วิธีแก้ปัญหา อย่างไรก็ตามฉันไม่แน่ใจในสิ่งที่มีความหมายโดย "คุณไม่สูงพอใน call stack" คุณรู้หรือไม่ว่าสิ่งนี้หมายความว่าอย่างไร
เทรเวอร์

13
@threed: ดูคำตอบโดย aaronfrost วิธีที่ถูกต้องคือการใช้การเลื่อนเวลาเพื่อกระตุ้นการย่อยในรอบถัดไป มิฉะนั้นเหตุการณ์จะหายไปและไม่อัปเดตขอบเขตเลย
Marek

663

จากการสนทนาเมื่อเร็ว ๆ นี้กับ Angular guys ในหัวข้อนี้: สำหรับเหตุผลการพิสูจน์ในอนาคตคุณไม่ควรใช้$$phase

เมื่อกดสำหรับวิธี "ถูกต้อง" คำตอบคือตอนนี้

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

เมื่อเร็ว ๆ นี้ฉันพบปัญหานี้เมื่อเขียนบริการเชิงมุมเพื่อรวม facebook, google และ twitter APIs ซึ่งมีองศาที่แตกต่างกัน

นี่คือตัวอย่างจากภายในบริการ (เพื่อความกะทัดรัด, ส่วนที่เหลือของบริการ - ที่ตั้งค่าตัวแปร, การฉีด $ หมดเวลา ฯลฯ - ถูกทิ้งไว้)

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

โปรดทราบว่าอาร์กิวเมนต์การหน่วงเวลาสำหรับ $ หมดเวลาเป็นตัวเลือกและจะใช้ค่าเริ่มต้นเป็น 0 หากไม่ได้ตั้งค่า ( $ หมดเวลาเรียกใช้$ browser.deferซึ่งค่าเริ่มต้นเป็น 0 หากไม่ได้ตั้งค่าการหน่วงเวลา )

เล็ก ๆ น้อย ๆ ที่ไม่ได้ใช้งานง่าย แต่นั่นเป็นคำตอบจากพวกที่เขียน Angular ดังนั้นมันดีพอสำหรับฉัน!


5
ฉันวิ่งเข้าไปในหลาย ๆ ครั้งในคำสั่งของฉัน คือการเขียนหนึ่งสำหรับ redactor และนี่เป็นการทำงานที่สมบูรณ์แบบ ฉันอยู่ที่การพบปะกับแบรดกรีนและเขาบอกว่า Angular 2.0 จะมีขนาดใหญ่โดยไม่ต้องใช้วงจรย่อยโดยใช้ความสามารถในการสังเกตแบบดั้งเดิมของ JS และใช้โพลีฟิลสำหรับเบราว์เซอร์ที่ไม่มีสิ่งนั้น ณ จุดนี้เราไม่จำเป็นต้องทำสิ่งนี้อีกต่อไป :)
Michael J. Calkins

เมื่อวานนี้ฉันเห็นปัญหาที่การเรียก selectize.refreshItems () ภายใน $ timeoutทำให้เกิดข้อผิดพลาดย่อยซ้ำแบบซ้ำ ความคิดใดที่เป็นไปได้?
iwein

3
ถ้าคุณใช้$timeoutมากกว่าพื้นเมืองsetTimeoutทำไมคุณไม่ใช้$windowแทนพื้นเมืองwindow?
LeeGee

2
@LeeGee: จุดที่ใช้$timeoutในกรณีนี้คือ$timeoutเพื่อให้แน่ใจว่ามีการอัปเดตขอบเขตเชิงมุมอย่างถูกต้อง หากการแยกย่อย $ ไม่ดำเนินการจะทำให้การแยกย่อย $ ใหม่ทำงาน
ความกลัว

2
@webicy นั่นไม่ใช่สิ่งที่ เมื่อเนื้อความของฟังก์ชั่นที่ส่งไปยังการหมดเวลาของ $ สัญญาได้รับการแก้ไขแล้ว! cancelมันไม่มีเหตุผลอะไรเลย จากเอกสาร : "ด้วยเหตุนี้สัญญาจะได้รับการแก้ไขด้วยการปฏิเสธ" คุณไม่สามารถแก้ไขสัญญาที่ได้รับการแก้ไขได้ การยกเลิกของคุณจะไม่ทำให้เกิดข้อผิดพลาด แต่จะไม่ทำสิ่งใดในเชิงบวกเช่นกัน
daemonexmachina

324

วงจรย่อยคือการโทรแบบซิงโครนัส มันจะไม่ให้การควบคุมเหตุการณ์วนรอบของเบราว์เซอร์จนกว่าจะเสร็จสิ้น มีสองสามวิธีในการจัดการกับสิ่งนี้ วิธีที่ง่ายที่สุดในการจัดการกับสิ่งนี้คือการใช้ $ timeout ในตัวและวิธีที่สองคือหากคุณใช้เครื่องหมายขีดล่างหรือ Lodash (และคุณควรจะเป็น) ให้เรียกสิ่งต่อไปนี้:

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

หรือถ้าคุณมี lodash:

_.defer(function(){$scope.$apply();});

เราลองวิธีแก้ปัญหาหลายอย่างและเราเกลียดการฉีด $ rootScope ลงในตัวควบคุมทิศทางและแม้แต่โรงงานบางแห่งของเรา ดังนั้นการหมดเวลาของ $ และ _.defer จึงเป็นสิ่งที่เราโปรดปราน วิธีการเหล่านี้ประสบความสำเร็จในการบอกเชิงมุมให้รอจนกระทั่งภาพเคลื่อนไหววนรอบถัดไปซึ่งจะรับประกันว่าขอบเขตปัจจุบันการใช้ $ จะจบลงแล้ว


2
สิ่งนี้เทียบได้กับการใช้ $ timeout (... ) หรือไม่ ฉันเคยใช้การหมดเวลาของ $ ในหลาย ๆ กรณีเพื่อรอรอบการแข่งขันรอบถัดไปและดูเหมือนว่าจะทำงานได้ดี - ใครรู้ว่ามีเหตุผลที่จะไม่ใช้การหมดเวลา $
เทรเวอร์

9
underscore.jsนี้ควรจริงๆเท่านั้นจะใช้ในกรณีที่คุณกำลังใช้อยู่แล้ว โซลูชันนี้ไม่คุ้มค่าที่จะนำเข้าไลบรารีขีดล่างทั้งหมดเพียงเพื่อใช้งานdeferฟังก์ชัน ฉันชอบ$timeoutวิธีการแก้ปัญหามากเพราะทุกคนสามารถเข้าถึง$timeoutมุมได้โดยไม่ต้องพึ่งห้องสมุดอื่น
Tennisgent

10
จริง ... แต่ถ้าคุณไม่ได้ใช้ขีดเส้นใต้หรือลอด ... คุณต้องประเมินสิ่งที่คุณกำลังทำ libs สองตัวนั้นเปลี่ยนวิธีการดูรหัส
frosty

2
เรามีบ้านพักอาศัยเป็นที่พึ่งพิงของ Restangular (เรากำลังจะกำจัด Restangular เพื่อสนับสนุน ng-route เร็ว ๆ นี้) ฉันคิดว่ามันเป็นคำตอบที่ดี แต่ก็ไม่ดีที่จะสมมติว่าคนต้องการใช้ขีดล่าง / lodash โดยทั้งหมดหมายความว่า libs นั้นใช้ได้ถ้าคุณใช้พอ ... วันนี้ฉันใช้วิธี ES5 ซึ่งล้าง 98% ของเหตุผลที่ฉันใช้เพื่อใส่เครื่องหมายขีดล่าง
BradGreens

2
คุณถูกต้อง @SgtPooki ฉันแก้ไขคำตอบเพื่อรวมตัวเลือกในการใช้ $ timeout ด้วย $ timeout และ _.defer ทั้งคู่จะรอจนกว่าลูปแอนิเมชันถัดไปซึ่งจะทำให้แน่ใจว่าขอบเขตปัจจุบัน $ Apply สิ้นสุดลงแล้ว ขอบคุณที่ทำให้ฉันซื่อสัตย์และขอให้ฉันอัปเดตคำตอบที่นี่
หนาวจัด

267

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

สิ่งที่คุณควรรู้

  • $$phase เป็นกรอบส่วนตัวและมีเหตุผลที่ดีสำหรับสิ่งนั้น

  • $timeout(callback)จะรอจนกว่าปัจจุบันย่อยวงจร (ถ้ามี) $applyจะทำจากนั้นดำเนินการเรียกกลับทำงานแล้วในตอนท้ายเต็ม

  • $timeout(callback, delay, false)จะทำเช่นเดียวกัน (โดยมีการหน่วงเวลาเป็นตัวเลือกก่อนดำเนินการติดต่อกลับ) แต่จะไม่ดำเนินการ$apply(อาร์กิวเมนต์ที่สาม) ซึ่งจะบันทึกการทำงานหากคุณไม่ได้แก้ไขโมเดลเชิงมุม (ขอบเขต $)

  • $scope.$apply(callback)จะเรียกใช้งานในสิ่งอื่น ๆ$rootScope.$digestซึ่งหมายความว่ามันจะ redigest ขอบเขตของแอปพลิเคชันและลูก ๆ ของมันใหม่แม้ว่าคุณจะอยู่ในขอบเขตที่แยกจากกัน

  • $scope.$digest()จะซิงค์โมเดลของมันกับมุมมอง แต่จะไม่แยกย่อยขอบเขตของพาเรนต์ซึ่งสามารถบันทึกการแสดงจำนวนมากเมื่อทำงานกับส่วนที่แยกต่างหากของ HTML ของคุณด้วยขอบเขตแยก (จากคำสั่งส่วนใหญ่) $ digest ไม่ใช้การเรียกกลับ: คุณเรียกใช้รหัสจากนั้นทำการแยกย่อย

  • $scope.$evalAsync(callback)ได้รับการแนะนำให้รู้จักกับ angularjs 1.2 และอาจจะแก้ปัญหาส่วนใหญ่ของคุณ โปรดอ้างอิงถึงย่อหน้าสุดท้ายเพื่อเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้

  • หากคุณได้รับ$digest already in progress errorแล้วสถาปัตยกรรมของคุณผิด: คุณไม่จำเป็นต้อง redigest ขอบเขตของคุณหรือคุณไม่ควรรับผิดชอบ (ดูด้านล่าง)

วิธีจัดโครงสร้างรหัสของคุณ

เมื่อคุณได้รับข้อผิดพลาดนั้นคุณกำลังพยายามแยกแยะขอบเขตของคุณในขณะที่กำลังดำเนินการอยู่: เนื่องจากคุณไม่ทราบสถานะของขอบเขตของคุณ ณ จุดนั้นคุณไม่รับผิดชอบในการจัดการกับการย่อย

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

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

อัปเดตตั้งแต่ Angularjs 1.2

ใหม่วิธีการที่มีประสิทธิภาพได้รับการเพิ่ม $ $evalAsyncขอบเขตใด โดยพื้นฐานแล้วมันจะทำการเรียกกลับภายในวงจรย่อยปัจจุบันถ้าเกิดขึ้นไม่เช่นนั้นวงจรย่อยใหม่จะเริ่มดำเนินการเรียกกลับ

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

ในกรณีเหล่านี้และอื่น ๆ ทั้งหมดที่คุณมีให้!$scope.$$phaseแน่ใจว่าได้ใช้$scope.$evalAsync( callback )


4
$timeoutถูกวิพากษ์วิจารณ์ในการผ่าน คุณสามารถให้เหตุผลเพิ่มเติมเพื่อหลีกเลี่ยงได้$timeoutหรือไม่
mlhDev

88

วิธีการช่วยเหลือตัวเล็กที่มีประโยชน์เพื่อให้กระบวนการนี้แห้ง:

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

6
safeApply ของคุณช่วยให้ฉันเข้าใจสิ่งที่เกิดขึ้นมากกว่าสิ่งอื่นใด ขอบคุณสำหรับการโพสต์ที่
Jason More

4
ฉันกำลังจะทำสิ่งเดียวกัน แต่ไม่ได้ทำสิ่งนี้หมายความว่ามีโอกาสที่การเปลี่ยนแปลงที่เราทำใน fn () จะไม่สามารถเห็นได้โดย $ digest จะเป็นการดีกว่าหรือไม่ที่จะชะลอการใช้งานฟังก์ชั่นสมมติว่าเป็นช่วง $$ $$ === '$ digest'
Spencer Alger

ฉันเห็นด้วยบางครั้ง $ Apply () ใช้เพื่อเรียกข้อมูลสรุปเพียงแค่เรียก fn ด้วยตัวเอง ... นั่นจะไม่ส่งผลให้เกิดปัญหาหรือไม่
CMCDragonkai

1
ฉันรู้สึกว่า scope.$apply(fn);ควรเป็นscope.$apply(fn());เพราะ fn () จะใช้งานฟังก์ชันและไม่ใช่ fn โปรดช่วยฉันในที่ที่ฉันผิด
madhu131313

1
@ZenOut การเรียกใช้ $ นำไปใช้สนับสนุนอาร์กิวเมนต์หลายประเภทรวมถึงฟังก์ชัน หากผ่านฟังก์ชั่นมันจะประเมินฟังก์ชั่น
boxmein

33

ฉันมีปัญหาเดียวกันกับสคริปต์ของบุคคลที่สามเช่น CodeMirror เช่นและ Krpano และแม้แต่การใช้วิธีการที่ปลอดภัยที่กล่าวถึงที่นี่ยังไม่ได้แก้ไขข้อผิดพลาดสำหรับฉัน

แต่สิ่งที่แก้ไขได้คือใช้บริการ $ 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

ดูhttp://docs.angularjs.org/error/$rootScope:inprog

ปัญหาเกิดขึ้นเมื่อคุณมีการเรียกไป$applyที่บางครั้งเรียกใช้แบบอะซิงโครนัสนอกรหัส Angular (เมื่อใช้ $ Apply ควรใช้) และบางครั้งจะซิงโครไนซ์ภายในโค้ด Angular (ซึ่งทำให้เกิด$digest already in progressข้อผิดพลาด)

สิ่งนี้อาจเกิดขึ้นเช่นเมื่อคุณมีไลบรารีที่ดึงรายการจากเซิร์ฟเวอร์แบบอะซิงโครนัสและแคช ในครั้งแรกที่มีการร้องขอรายการนั้นจะถูกดึงแบบอะซิงโครนัสเพื่อไม่ให้มีการปิดกั้นการเรียกใช้โค้ด อย่างไรก็ตามในครั้งที่สองรายการนั้นอยู่ในแคชอยู่แล้วดังนั้นจึงสามารถเรียกคืนได้พร้อมกัน

วิธีป้องกันข้อผิดพลาดนี้คือเพื่อให้แน่ใจว่ารหัสที่เรียก$applyใช้นั้นทำงานแบบอะซิงโครนัส สิ่งนี้สามารถทำได้โดยการเรียกใช้รหัสของคุณภายในการโทร$timeoutด้วยโดยตั้งค่าการหน่วงเวลาเป็น0(ซึ่งเป็นค่าเริ่มต้น) อย่างไรก็ตามการเรียกรหัสของคุณภายใน$timeoutนั้นไม่จำเป็นต้องโทรออก$applyเพราะการหมดเวลา $ จะทำให้เกิด$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 เสมอ (เช่นการเรียกใช้ $ ของคุณจะเกิดขึ้นภายในการเรียกกลับที่ถูกเรียกโดยรหัสนอกรหัส Angular ของคุณ)

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


ขอบคุณสิ่งนี้ใช้ได้กับกรณีของฉันซึ่งฉันไม่ได้โทรหา$applyตัวเอง
ariscris

5
ความแตกต่างที่สำคัญคือ$applyเป็นซิงโคร (โทรกลับของมันจะถูกดำเนินการแล้วรหัสต่อไปนี้ใช้ $) ในขณะที่$timeoutไม่ได้: setTimeoutรหัสปัจจุบันต่อไปนี้จะถูกดำเนินการหมดเวลาแล้วกองใหม่เริ่มต้นด้วยการเรียกกลับของมันราวกับว่าคุณกำลังใช้ ซึ่งอาจนำไปสู่ข้อบกพร่องด้านกราฟิกหากคุณอัพเดตสองครั้งในรุ่นเดียวกัน: $timeoutจะรอให้มุมมองรับการฟื้นฟูก่อนที่จะอัปเดตอีกครั้ง
floribon

ขอบคุณแน่นอนตกลง ฉันมีวิธีการที่เรียกว่าเป็นผลมาจากกิจกรรมการติดตาม $ และพยายามอัปเดต UI ก่อนที่ตัวกรองภายนอกของฉันจะดำเนินการเสร็จสิ้น การใช้ฟังก์ชั่นการหมดเวลา $ ทำงานให้ฉัน
djmarquette

28

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


เฮ้ฉันใช้เวลาทั้งวันเพื่อตรวจสอบว่า AngularJS ไม่สามารถดูการผูก "อย่างน่าอัศจรรย์" และฉันควรผลักเขาด้วยเงิน $ $ ()
OZ_

หมายความว่าyou're not updating the the model correctlyอย่างไร $scope.err_message = 'err message';ปรับปรุงไม่ถูกต้อง?
OZ_

2
ครั้งเดียวที่คุณต้องโทร$apply()คือเมื่อคุณอัปเดตโมเดล "นอก" ของเชิงมุม (เช่นจากปลั๊กอิน jQuery) มันง่ายที่จะตกหลุมพรางของมุมมองที่ไม่ถูกต้องและคุณก็โยน$apply()มันไปทุกที่ซึ่งจบลงด้วยข้อผิดพลาดที่เห็นใน OP เมื่อฉันบอกว่าyou're not updating the the model correctlyฉันแค่หมายถึงตรรกะทางธุรกิจทั้งหมดไม่ถูกต้องเติมสิ่งที่อาจอยู่ในขอบเขตซึ่งนำไปสู่มุมมองที่ไม่ได้มองตามที่คาดไว้
dnc253

@ dnc253 ฉันเห็นด้วยและฉันเขียนคำตอบ เมื่อรู้สิ่งที่ฉันรู้ตอนนี้ฉันจะใช้ $ timeout (function () {... }); มันทำสิ่งเดียวกับ _.defer พวกเขาทั้งสองเลื่อนไปที่ลูปแอนิเมชันถัดไป
หนาวจัด


11

คุณยังสามารถใช้ evalAsync มันจะทำงานบางครั้งหลังจากเสร็จสิ้นการย่อย!

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

10

ก่อนอื่นอย่าแก้ไขด้วยวิธีนี้

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}

มันไม่สมเหตุสมผลเพราะ $ phase เป็นเพียงแฟล็กบูลีนสำหรับวงจรการแยกย่อย $ ดังนั้นบางครั้งการใช้ $ ของคุณจะไม่ทำงาน และจำไว้ว่ามันเป็นการปฏิบัติที่ไม่ดี

ใช้แทน $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(); 
});

9

บางครั้งคุณจะยังคงได้รับข้อผิดพลาดหากคุณใช้วิธีนี้ ( https://stackoverflow.com/a/12859093/801426 )

ลองสิ่งนี้:

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

5
ใช้ทั้ง! $ scope. $$ phase และ! $ scope. $ root. $ phase (ไม่ใช่! $ rootScope. $ root. $$ phase) ใช้งานได้สำหรับฉัน 1
asprotte

2
$rootScopeและanyScope.$rootเป็นผู้ชายคนเดียวกัน $rootScope.$rootซ้ำซ้อน
floribon


5

ลองใช้

$scope.applyAsync(function() {
    // your code
});

แทน

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

$ ApplyAsync กำหนดเวลาการเรียกใช้ $ จะเกิดขึ้นในภายหลัง สิ่งนี้สามารถใช้เพื่อจัดคิวหลายนิพจน์ซึ่งจำเป็นต้องประเมินในไดเจสต์เดียวกัน

หมายเหตุ: ภายใน $ digest, $ ApplyAsync () จะล้างออกเฉพาะเมื่อขอบเขตปัจจุบันคือ $ rootScope ซึ่งหมายความว่าถ้าคุณเรียกใช้ $ แยกย่อยในขอบเขตลูกมันจะไม่ล้างโดยปริยายคิว $ 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.3

  1. เอกสาร AngularJs

4

ฉันอยากจะแนะนำให้คุณใช้เหตุการณ์ที่กำหนดเองแทนการทริกเกอร์วงจรย่อย

ฉันพบว่าการเผยแพร่กิจกรรมที่กำหนดเองและการลงทะเบียนผู้ฟังสำหรับเหตุการณ์นี้เป็นทางออกที่ดีสำหรับการกระตุ้นการกระทำที่คุณต้องการให้เกิดขึ้นไม่ว่าคุณจะอยู่ในรอบย่อยหรือไม่

โดยการสร้างเหตุการณ์ที่กำหนดเองคุณจะมีประสิทธิภาพมากขึ้นด้วยรหัสของคุณเพราะคุณเป็นเพียงการกระตุ้นให้ผู้ฟังที่สมัครเป็นสมาชิกของเหตุการณ์ดังกล่าวและไม่เรียกนาฬิกาทั้งหมดที่ถูกผูกไว้กับขอบเขตเช่นเดียวกับถ้าคุณเรียกใช้ขอบเขต

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


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

3

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

2

ฉันได้รับสามารถที่จะแก้ปัญหานี้โดยการเรียก$evalแทน$applyในสถานที่ที่ฉันรู้ว่า$digestฟังก์ชั่นจะทำงาน

ตามเอกสารนั้น$applyโดยทั่วไปแล้วสิ่งนี้:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

ในกรณีของการng-clickเปลี่ยนแปลงตัวแปรภายในขอบเขตและ $ นาฬิกาในตัวแปรที่มีการเปลี่ยนแปลงตัวแปรอื่น ๆ $appliedซึ่งจะต้องมีการ ขั้นตอนสุดท้ายนี้ทำให้เกิดข้อผิดพลาด "สรุปย่อกำลังดำเนินการอยู่"

โดยการแทนที่$applyด้วย$evalภายในแสดงออกนาฬิกาตัวแปรขอบเขตได้รับการปรับปรุงคาดว่าเป็น

ดังนั้นดูเหมือนว่าหากการแยกย่อยกำลังจะดำเนินการต่อไปเนื่องจากมีการเปลี่ยนแปลงอื่น ๆ ภายใน Angular $evalคุณจำเป็นต้องทำทุกอย่าง



1

การทำความเข้าใจว่าเอกสารเชิงมุมโทรตรวจสอบต่อต้านรูปแบบที่ผมพยายามที่จะได้รับและ$$phase$timeout_.deferทำงาน

วิธีการหมดเวลาและรอการตัดบัญชีสร้างแฟลชของ{{myVar}}เนื้อหาที่ไม่ได้แยกวิเคราะห์ในโดมเช่นFOUT foutสำหรับฉันนี้ไม่เป็นที่ยอมรับ มันทำให้ฉันไม่ต้องบอกอะไรมากเกี่ยวกับสิ่งที่ดื้อรั้นและไม่มีทางเลือกที่เหมาะสม

สิ่งเดียวที่ทำงานได้ทุกครั้งคือ:

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

ฉันไม่เข้าใจถึงอันตรายของวิธีนี้หรือทำไมมันจึงถูกอธิบายว่าเป็นการแฮ็คโดยผู้คนในความคิดเห็นและทีมเชิงมุม คำสั่งดูเหมือนแม่นยำและอ่านง่าย:

"ทำการแยกข้อมูลยกเว้นว่ามีเหตุการณ์เกิดขึ้นแล้ว"

ใน CoffeeScript มันยอดเยี่ยมกว่า:

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

มีปัญหาอะไรกับเรื่องนี้? มีทางเลือกอื่นที่จะไม่สร้าง FOUT หรือไม่? $ safeApplyดูดี แต่ใช้$$phaseวิธีการตรวจสอบด้วย


1
ฉันชอบที่จะเห็นการตอบสนองที่มีข้อมูลสำหรับคำถามนี้!
Ben Wheeler

มันเป็นแฮ็คเพราะมันหมายความว่าคุณพลาดบริบทหรือไม่เข้าใจโค้ดในจุดนี้: ไม่ว่าคุณจะอยู่ในวงจรการย่อยเชิงมุมและคุณไม่ต้องการมัน หากคุณไม่สามารถรู้ได้ว่าในจุดนั้นคุณจะไม่รับผิดชอบในการย่อย
floribon

1

นี่คือบริการ utils ของฉัน:

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

ฉันใช้วิธีนี้และดูเหมือนว่าจะทำงานได้อย่างสมบูรณ์แบบ 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

เมื่อฉันปิดการใช้งานดีบักเกอร์ข้อผิดพลาดจะไม่เกิดขึ้นอีก ในกรณีของฉันมันเป็นเพราะ debugger หยุดการเรียกใช้โค้ด


0

คล้ายกับคำตอบข้างต้น แต่สิ่งนี้ได้ผลสำหรับฉัน ... ในการบริการเพิ่ม:

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

0

คุณสามารถใช้ได้

$timeout

เพื่อป้องกันข้อผิดพลาด

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

จะเป็นอย่างไรถ้าฉันไม่ต้องการใช้การหมดเวลาของ $ $
rahim.nagori

0

โดยทั่วไปปัญหาจะมาเมื่อเรากำลังขอให้เชิงมุมเรียกใช้วัฏจักรการย่อยแม้ว่ามันจะอยู่ในกระบวนการซึ่งกำลังสร้างปัญหาให้เชิงมุมเพื่อทำความเข้าใจ ข้อยกเว้นที่ตามมาในคอนโซล
1. ไม่มีความหมายใด ๆ ในการเรียกใช้ขอบเขต $ Apply () ภายในฟังก์ชัน $ timeout เพราะภายในจะทำเช่นเดียวกัน
2. โค้ดจะไปพร้อมกับฟังก์ชั่นของ vanilla JavaScript เพราะมันไม่ได้เป็นเชิงมุมที่กำหนดไว้เช่น setTimeout
3. ในการทำเช่นนั้นคุณสามารถใช้

if (! scope. $$ phase) {
scope. $ evalAsync (function () {

}); }


0
        let $timeoutPromise = null;
        $timeout.cancel($timeoutPromise);
        $timeoutPromise = $timeout(() => {
            $scope.$digest();
        }, 0, false);

นี่เป็นทางออกที่ดีในการหลีกเลี่ยงข้อผิดพลาดนี้และหลีกเลี่ยงการใช้ $

คุณสามารถรวมสิ่งนี้กับ debounce (0) ถ้าโทรตามเหตุการณ์ภายนอก ด้านบนคือ 'debounce' ที่เราใช้และตัวอย่างเต็มของรหัส

.factory('debounce', [
    '$timeout',
    function ($timeout) {

        return function (func, wait, apply) {
            // apply default is true for $timeout
            if (apply !== false) {
                apply = true;
            }

            var promise;
            return function () {
                var cntx = this,
                    args = arguments;
                $timeout.cancel(promise);
                promise = $timeout(function () {
                    return func.apply(cntx, args);
                }, wait, apply);
                return promise;
            };
        };
    }
])

และโค้ดของตัวเองเพื่อรับฟังเหตุการณ์บางอย่างและเรียก $ digest เพียงแค่ $ scope ที่คุณต้องการ

        let $timeoutPromise = null;
        let $update = debounce(function () {
            $timeout.cancel($timeoutPromise);
            $timeoutPromise = $timeout(() => {
                $scope.$digest();
            }, 0, false);
        }, 0, false);

        let $unwatchModelChanges = $scope.$root.$on('updatePropertiesInspector', function () {
            $update();
        });


        $scope.$on('$destroy', () => {
            $timeout.cancel($update);
            $timeout.cancel($timeoutPromise);
            $unwatchModelChanges();
        });

-3

พบสิ่งนี้: https://coderwall.com/p/ngismaโดยที่ Nathan Walker (ใกล้ด้านล่างของหน้า) แนะนำมัณฑนากรใน $ rootScope เพื่อสร้าง func 'safeApply' รหัส:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);

-7

นี่จะเป็นการแก้ปัญหาของคุณ:

if(!$scope.$$phase) {
  //TODO
}

อย่าทำถ้า (! $ scope. $$ phase) $ scope $ Apply () มันหมายถึง $ scope ของคุณ $ Apply () ไม่สูงพอใน call stack
MGot90
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.