Skip to content

Commit f8b6572

Browse files
gkalpakellimist
authored andcommitted
fix($rootScope): when adding/removing watchers during $digest
Previously, adding a watcher during a `$digest` (i.e. from within a watcher), would result in the next watcher getting skipped. Similarly, removing a watcher during a `$digest` could result in the current watcher being run twice (if the removed watcher had not run yet in the current `$digest`). This commit fixes both cases by keeping track of the current watcher index during a digest and properly updating it when adding/removing watchers. Fixes angular#15422 Closes angular#15424
1 parent 0262495 commit f8b6572

File tree

2 files changed

+84
-8
lines changed

2 files changed

+84
-8
lines changed

src/ng/rootScope.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -416,15 +416,21 @@ function $RootScopeProvider() {
416416

417417
if (!array) {
418418
array = scope.$$watchers = [];
419+
array.$$digestWatchIndex = -1;
419420
}
420421
// we use unshift since we use a while loop in $digest for speed.
421422
// the while loop reads in reverse order.
422423
array.unshift(watcher);
424+
array.$$digestWatchIndex++;
423425
incrementWatchersCount(this, 1);
424426

425427
return function deregisterWatch() {
426-
if (arrayRemove(array, watcher) >= 0) {
428+
var index = arrayRemove(array, watcher);
429+
if (index >= 0) {
427430
incrementWatchersCount(scope, -1);
431+
if (index < array.$$digestWatchIndex) {
432+
array.$$digestWatchIndex--;
433+
}
428434
}
429435
lastDirtyWatch = null;
430436
};
@@ -757,7 +763,6 @@ function $RootScopeProvider() {
757763
$digest: function() {
758764
var watch, value, last, fn, get,
759765
watchers,
760-
length,
761766
dirty, ttl = TTL,
762767
next, current, target = this,
763768
watchLog = [],
@@ -798,10 +803,10 @@ function $RootScopeProvider() {
798803
do { // "traverse the scopes" loop
799804
if ((watchers = current.$$watchers)) {
800805
// process our watches
801-
length = watchers.length;
802-
while (length--) {
806+
watchers.$$digestWatchIndex = watchers.length;
807+
while (watchers.$$digestWatchIndex--) {
803808
try {
804-
watch = watchers[length];
809+
watch = watchers[watchers.$$digestWatchIndex];
805810
// Most common watches are on primitives, in which case we can short
806811
// circuit it with === operator, only when === fails do we use .equals
807812
if (watch) {

test/ng/rootScopeSpec.js

+74-3
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,78 @@ describe('Scope', function() {
498498
expect(watch2).toHaveBeenCalled();
499499
}));
500500

501+
502+
it('should not skip watchers when adding new watchers during digest',
503+
inject(function($rootScope) {
504+
var log = [];
505+
506+
var watchFn1 = function() { log.push(1); };
507+
var watchFn2 = function() { log.push(2); };
508+
var watchFn3 = function() { log.push(3); };
509+
var addWatcherOnce = function(newValue, oldValue) {
510+
if (newValue === oldValue) {
511+
$rootScope.$watch(watchFn3);
512+
}
513+
};
514+
515+
$rootScope.$watch(watchFn1, addWatcherOnce);
516+
$rootScope.$watch(watchFn2);
517+
518+
$rootScope.$digest();
519+
520+
expect(log).toEqual([1, 2, 3, 1, 2, 3]);
521+
})
522+
);
523+
524+
525+
it('should not run the current watcher twice when removing a watcher during digest',
526+
inject(function($rootScope) {
527+
var log = [];
528+
var removeWatcher3;
529+
530+
var watchFn3 = function() { log.push(3); };
531+
var watchFn2 = function() { log.push(2); };
532+
var watchFn1 = function() { log.push(1); };
533+
var removeWatcherOnce = function(newValue, oldValue) {
534+
if (newValue === oldValue) {
535+
removeWatcher3();
536+
}
537+
};
538+
539+
$rootScope.$watch(watchFn1, removeWatcherOnce);
540+
$rootScope.$watch(watchFn2);
541+
removeWatcher3 = $rootScope.$watch(watchFn3);
542+
543+
$rootScope.$digest();
544+
545+
expect(log).toEqual([1, 2, 1, 2]);
546+
})
547+
);
548+
549+
550+
it('should not skip watchers when removing itself during digest',
551+
inject(function($rootScope) {
552+
var log = [];
553+
var removeWatcher1;
554+
555+
var watchFn3 = function() { log.push(3); };
556+
var watchFn2 = function() { log.push(2); };
557+
var watchFn1 = function() { log.push(1); };
558+
var removeItself = function() {
559+
removeWatcher1();
560+
};
561+
562+
removeWatcher1 = $rootScope.$watch(watchFn1, removeItself);
563+
$rootScope.$watch(watchFn2);
564+
$rootScope.$watch(watchFn3);
565+
566+
$rootScope.$digest();
567+
568+
expect(log).toEqual([1, 2, 3, 2, 3]);
569+
})
570+
);
571+
572+
501573
it('should not infinitely digest when current value is NaN', inject(function($rootScope) {
502574
$rootScope.$watch(function() { return NaN;});
503575

@@ -598,7 +670,7 @@ describe('Scope', function() {
598670

599671
$rootScope.$digest();
600672

601-
expect(log).toEqual(['watch1', 'watchAction1', 'watch1', 'watch3', 'watchAction3',
673+
expect(log).toEqual(['watch1', 'watchAction1', 'watch3', 'watchAction3',
602674
'watch1', 'watch3']);
603675
scope.$destroy();
604676
log.reset();
@@ -895,8 +967,7 @@ describe('Scope', function() {
895967
$rootScope.$watch(log.fn('w5'), log.fn('w5action'));
896968
});
897969
$rootScope.$digest();
898-
expect(log).toEqual(['w1', 'w2', 'w3', 'w4', 'w4action',
899-
'w1', 'w2', 'w3', 'w4', 'w5', 'w5action',
970+
expect(log).toEqual(['w1', 'w2', 'w3', 'w4', 'w4action', 'w5', 'w5action',
900971
'w1', 'w2', 'w3', 'w4', 'w5']);
901972
}));
902973

0 commit comments

Comments
 (0)