Mengapa menggunakan if (! $ Scope. $$ phase) $ scope. $ Apply () sebuah anti-pattern?

92

Terkadang saya perlu menggunakan $scope.$applykode saya dan terkadang muncul error "intisari sudah dalam proses". Jadi saya mulai menemukan jalan keluarnya dan menemukan pertanyaan ini: AngularJS: Mencegah error $ digest sedang berlangsung saat memanggil $ scope. $ Apply () . Namun di komentar (dan di wiki sudut) Anda dapat membaca:

Jangan lakukan jika (! $ Scope. $$ phase) $ scope. $ Apply (), artinya $ scope Anda. $ Apply () tidak cukup tinggi dalam tumpukan panggilan.

Jadi sekarang saya punya dua pertanyaan:

  1. Mengapa tepatnya ini merupakan anti-pola?
  2. Bagaimana cara menggunakan $ scope. $ Apply dengan aman?

"Solusi" lain untuk mencegah kesalahan "intisari sedang dalam proses" tampaknya menggunakan $ timeout:

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

Apakah itu cara untuk pergi? Apakah ini lebih aman? Jadi, inilah pertanyaan sebenarnya: Bagaimana saya bisa sepenuhnya menghilangkan kemungkinan kesalahan "intisari sudah dalam proses"?

PS: Saya hanya menggunakan $ scope. $ Apply in non-angularjs callbacks yang tidak sinkron. (sejauh yang saya tahu itu adalah situasi di mana Anda harus menggunakan $ scope. $ apply jika Anda ingin perubahan Anda diterapkan)

Dominik Goltermann
sumber
Dari pengalaman saya, Anda harus selalu tahu, apakah Anda memanipulasi scopedari dalam sudut atau dari luar sudut. Jadi menurut Anda ini selalu tahu, apakah Anda perlu menelepon scope.$applyatau tidak. Dan jika Anda menggunakan kode yang sama untuk kedua scopemanipulasi sudut / non-sudut , Anda salah melakukannya, itu harus selalu dipisahkan ... jadi pada dasarnya jika Anda mengalami kasus di mana Anda perlu memeriksa scope.$$phase, kode Anda tidak dirancang dengan cara yang benar, dan selalu ada cara untuk melakukannya dengan 'cara yang benar'
doodeec
1
Saya hanya menggunakan ini di panggilan balik non-sudut (!) Inilah mengapa saya bingung
Dominik Goltermann
2
jika itu non-sudut, itu tidak akan digest already in progress
menimbulkan
1
itulah yang saya pikir. Masalahnya adalah: itu tidak selalu membuang kesalahan. Hanya sesekali. Kecurigaan saya adalah bahwa aplikasi bertabrakan DENGAN KESEMPATAN dengan intisari lain. Apakah itu mungkin?
Dominik Goltermann
Saya tidak berpikir itu mungkin jika callback benar-benar non-sudut
doodeec

Jawaban:

113

Setelah menggali lebih dalam, saya dapat menjawab pertanyaan apakah selalu aman untuk digunakan $scope.$apply . Jawaban singkatnya adalah ya.

Jawaban panjang:

Karena cara browser Anda menjalankan Javascript, tidak mungkin dua panggilan intisari bertabrakan secara kebetulan .

Kode JavaScript yang kami tulis tidak semuanya berjalan sekaligus, melainkan dijalankan secara bergantian. Masing-masing belokan ini berjalan tanpa gangguan dari awal hingga akhir, dan ketika belokan sedang berjalan, tidak ada lagi yang terjadi di browser kami. (dari http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Karenanya kesalahan "intisari sudah dalam proses" hanya dapat terjadi dalam satu situasi: Ketika $ apply dikeluarkan di dalam $ apply lain, misalnya:

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

Situasi ini tidak bisa muncul jika kita menggunakan $ scope.apply dalam callback non-angularjs murni, seperti misalnya callback setTimeout. Jadi kode berikut ini 100% antipeluru dan tidak perlu melakukan aif (!$scope.$$phase) $scope.$apply()

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

bahkan yang ini aman:

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

Apa yang TIDAK aman (karena $ timeout - seperti semua pembantu angularjs - sudah memanggil $scope.$applyAnda):

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

Ini juga menjelaskan mengapa penggunaan if (!$scope.$$phase) $scope.$apply()anti-pola. Anda tidak membutuhkannya jika Anda menggunakan $scope.$applydengan cara yang benar: Dalam callback js murni seperti setTimeoutmisalnya.

Baca http://jimhoskins.com/2012/12/17/angularjs-and-apply.html untuk penjelasan lebih rinci.

Dominik Goltermann
sumber
Saya mendapat contoh di mana saya membuat layanan dengan $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });saya benar-benar tidak tahu mengapa saya harus membuat $ apply di sini, karena saya menggunakan $ document.bind ..
Betty St
karena $ document hanyalah "JQuery atau jqLite wrapper untuk objek window.document browser." dan diimplementasikan sebagai berikut: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Tidak ada aplikasi di sana.
Dominik Goltermann
11
$timeoutsecara semantik berarti menjalankan kode setelah penundaan. Ini mungkin hal yang secara fungsional aman untuk dilakukan tetapi ini adalah peretasan. Seharusnya ada cara yang aman untuk menggunakan $ apply ketika Anda tidak dapat mengetahui apakah suatu $digestsiklus sedang berlangsung atau Anda sudah berada di dalam $apply.
John Strickler
1
alasan lain mengapa itu buruk: ia menggunakan variabel internal (fase $$) yang bukan bagian dari api publik dan mereka mungkin diubah dalam versi sudut yang lebih baru dan dengan demikian merusak kode Anda. Masalah Anda dengan pemicu peristiwa sinkronis cukup menarik
Dominik Goltermann
4
Pendekatan yang lebih baru adalah menggunakan $ scope. $ EvalAsync () yang dengan aman dijalankan dalam siklus intisari saat ini jika memungkinkan atau di siklus berikutnya. Lihat bennadel.com/blog/…
jaymjarri
16

Ini jelas merupakan anti-pola sekarang. Saya telah melihat intisari meledak bahkan jika Anda memeriksa fase $$. Anda hanya tidak seharusnya mengakses API internal yang dilambangkan dengan $$prefiks.

Kamu harus menggunakan

 $scope.$evalAsync();

karena ini adalah metode yang disukai di Angular ^ 1.4 dan secara khusus diekspos sebagai API untuk lapisan aplikasi.

FlavorScape
sumber
9

Dalam kasus apa pun saat intisari Anda sedang berlangsung dan Anda mendorong layanan lain untuk mencerna, itu hanya memberikan kesalahan yaitu intisari sudah dalam proses. jadi untuk menyembuhkan ini, Anda memiliki dua pilihan. Anda dapat memeriksa intisari lain yang sedang berlangsung seperti polling.

Pertama

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

jika kondisi di atas benar, maka Anda dapat menerapkan $ scope Anda. $ apply otherwies not and

solusi kedua adalah menggunakan $ timeout

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

ia tidak akan membiarkan intisari lainnya mulai sampai $ timeout menyelesaikan eksekusinya.

Lalit Sachdeva
sumber
1
suara negatif; Pertanyaannya secara khusus menanyakan mengapa TIDAK melakukan hal yang Anda gambarkan di sini, bukan untuk cara lain untuk meretasnya. Lihat jawaban luar biasa dari @gaul untuk mengetahui kapan harus digunakan $scope.$apply();.
PureSpider
Meskipun tidak menjawab pertanyaan: $timeoutadalah kuncinya! itu berhasil dan kemudian saya menemukan bahwa itu direkomendasikan juga.
Himel Nag Rana
Saya tahu sudah cukup terlambat untuk menambahkan komentar untuk ini 2 tahun kemudian, tetapi hati-hati ketika menggunakan $ timeout terlalu banyak, karena ini dapat membuat Anda terlalu banyak biaya dalam kinerja jika Anda tidak memiliki struktur aplikasi yang baik
cpoDesign
9

scope.$applymemicu $digestsiklus yang fundamental untuk pengikatan data 2 arah

Sebuah $digestsiklus memeriksa objek yaitu model (tepatnya $watch) yang dilampirkan untuk $scopemenilai apakah nilainya telah berubah dan jika mendeteksi perubahan maka diperlukan langkah-langkah yang diperlukan untuk memperbarui tampilan.

Sekarang ketika Anda menggunakan $scope.$applyAnda menghadapi kesalahan "Sudah dalam proses" sehingga cukup jelas bahwa $ digest sedang berjalan tetapi apa yang memicunya?

ans -> setiap $httppanggilan, semua ng-klik, ulangi, tampilkan, sembunyikan dll memicu $digestsiklus DAN BAGIAN TERBURUK JALAN SETIAP $ LINGKUP.

misalnya, laman Anda memiliki 4 pengontrol atau arahan A, B, C, D

Jika Anda memiliki 4 $scopeproperti di masing-masing properti, maka Anda memiliki total 16 $ properti cakupan di halaman Anda.

Jika Anda memicu $scope.$applydi pengontrol D maka a$digest siklus akan memeriksa semua 16 nilai !!! ditambah semua properti $ rootScope.

Jawaban -> tetapi $scope.$digestmemicu $digeston child dan cakupan yang sama sehingga hanya akan memeriksa 4 properti. Jadi jika Anda yakin bahwa perubahan D tidak akan mempengaruhi A, B, C maka gunakan $scope.$digest tidak $scope.$apply.

Jadi, sekadar ng-click atau ng-show / hide dapat memicu $digestsiklus pada lebih dari 100 properti bahkan ketika pengguna belum mengaktifkan peristiwa apa pun !

Rishul Matta
sumber
2
Ya saya menyadari ini terlambat ke proyek sayangnya. Tidak akan menggunakan Angular jika saya tahu ini sejak awal. Semua arahan standar mengaktifkan $ scope. $ Apply, yang secara bergantian memanggil $ rootScope. $ Digest, yang melakukan pemeriksaan kotor pada SEMUA cakupan. Keputusan desain yang buruk jika Anda bertanya kepada saya. Saya harus mengendalikan lingkup apa yang harus diperiksa kotor, karena SAYA TAHU BAGAIMANA DATA TERKAIT DENGAN LINGKUP INI!
MoonStom
0

Gunakan $timeout, itu cara yang disarankan.

Skenario saya adalah saya perlu mengubah item pada halaman berdasarkan data yang saya terima dari WebSocket. Dan karena berada di luar Angular, tanpa batas waktu $, satu-satunya model yang akan diubah tetapi tampilan tidak akan berubah. Karena Angular tidak tahu bahwa bagian data telah diubah.$timeoutpada dasarnya memberi tahu Angular untuk membuat perubahan di babak $ digest berikutnya.

Saya mencoba yang berikut ini juga dan berhasil. Perbedaannya bagi saya adalah $ batas waktu lebih jelas.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
sumber
Jauh lebih bersih untuk membungkus kode socket Anda dalam $ apply (seperti halnya Angular pada kode AJAX, yaitu $http). Jika tidak, Anda harus mengulangi kode ini di semua tempat.
timruffles
ini jelas tidak disarankan. Selain itu, Anda terkadang akan mendapatkan kesalahan melakukan ini jika $ scope memiliki fase $$. sebagai gantinya, Anda harus menggunakan $ scope. $ evalAsync ();
FlavorScape
Tidak perlu $scope.$applyjika Anda menggunakan setTimeoutatau$timeout
Kunal
-1

Saya menemukan solusi yang sangat keren:

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

menyuntikkan itu di mana Anda membutuhkan:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
bora89
sumber