diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index e03aa961dcf4..016fdfac77e9 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -301,7 +301,7 @@ function $InterpolateProvider() { exp: text, //just for compatibility with regular watchers created via $watch separators: separators, expressions: expressions, - $$watchDelegate: function (scope, listener, objectEquality) { + $$watchDelegate: function (scope, listener, objectEquality, deregisterNotifier) { var lastValue; return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { var currValue = compute(values); @@ -309,7 +309,7 @@ function $InterpolateProvider() { listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); } lastValue = currValue; - }, objectEquality); + }, objectEquality, deregisterNotifier); } }); } diff --git a/src/ng/parse.js b/src/ng/parse.js index 11cf501d9f83..2730e6cc3f5b 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1035,7 +1035,7 @@ function $ParseProvider() { } }; - function oneTimeWatch(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) { var unwatch, lastValue; return unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); @@ -1051,10 +1051,10 @@ function $ParseProvider() { } }); } - }, objectEquality); + }, objectEquality, deregisterNotifier); } - function oneTimeLiteralWatch(scope, listener, objectEquality, parsedExpression) { + function oneTimeLiteralWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) { var unwatch; return unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); @@ -1078,7 +1078,7 @@ function $ParseProvider() { } } - function constantWatch(scope, listener, objectEquality, parsedExpression) { + function constantWatch(scope, listener, objectEquality, deregisterNotifier, parsedExpression) { var unwatch; return unwatch = scope.$watch(function constantWatch(scope) { return parsedExpression(scope); @@ -1087,7 +1087,7 @@ function $ParseProvider() { listener.apply(this, arguments); } unwatch(); - }, objectEquality); + }, objectEquality, deregisterNotifier); } function addInterceptor(parsedExpression, interceptorFn) { diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 24b80a36ed4a..a52baf637634 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -322,13 +322,15 @@ function $RootScopeProvider(){ * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. + * @param {function()=} deregisterNotifier Function to call when the deregistration function + * get called. * @returns {function()} Returns a deregistration function for this listener. */ - $watch: function(watchExp, listener, objectEquality) { + $watch: function(watchExp, listener, objectEquality, deregisterNotifier) { var get = compileToFn(watchExp, 'watch'); if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get); + return get.$$watchDelegate(this, listener, objectEquality, deregisterNotifier, get); } var scope = this, array = scope.$$watchers, @@ -356,6 +358,9 @@ function $RootScopeProvider(){ return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; + if (isFunction(deregisterNotifier)) { + deregisterNotifier(); + } }; }, @@ -388,9 +393,9 @@ function $RootScopeProvider(){ var oldValues = new Array(watchExpressions.length); var newValues = new Array(watchExpressions.length); var deregisterFns = []; + var changeCount = 0; var self = this; - var changeReactionScheduled = false; - var firstRun = true; + var masterUnwatch; if (watchExpressions.length === 1) { // Special case size of one @@ -402,31 +407,29 @@ function $RootScopeProvider(){ } forEach(watchExpressions, function (expr, i) { - var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + var unwatch = self.$watch(expr, function watchGroupSubAction(value, oldValue) { newValues[i] = value; oldValues[i] = oldValue; - if (!changeReactionScheduled) { - changeReactionScheduled = true; - self.$evalAsync(watchGroupAction); + changeCount++; + }, false, function watchGroupDeregNotifier() { + arrayRemove(deregisterFns, unwatch); + if (!deregisterFns.length) { + masterUnwatch(); } }); - deregisterFns.push(unwatchFn); - }); - function watchGroupAction() { - changeReactionScheduled = false; + deregisterFns.push(unwatch); + }, this); - if (firstRun) { - firstRun = false; - listener(newValues, newValues, self); - } else { - listener(newValues, oldValues, self); - } - } + masterUnwatch = self.$watch(function watchGroupChangeWatch() { + return changeCount; + }, function watchGroupChangeAction(value, oldValue) { + listener(newValues, (value === oldValue) ? newValues : oldValues, self); + }); return function deregisterWatchGroup() { while (deregisterFns.length) { - deregisterFns.shift()(); + deregisterFns[0](); } }; }, diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 27661dfa0e45..fe8257aaafa9 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2893,21 +2893,21 @@ describe('$compile', function() { inject(function($rootScope) { compile('
'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' + expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> '=' $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(6); + expect(countWatches($rootScope)).toEqual(7); $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + expect(countWatches($rootScope)).toEqual(5); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.bar = 'baz'; $rootScope.$digest(); @@ -2927,21 +2927,21 @@ describe('$compile', function() { inject(function($rootScope) { compile('
'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> {{ }} + expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> {{ }} $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(4); // (- 2) -> bind-once in template + expect(countWatches($rootScope)).toEqual(5); // (- 2) -> bind-once in template $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.bar = 'baz'; $rootScope.$digest(); @@ -2964,18 +2964,18 @@ describe('$compile', function() { compile('
'); $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(6); // 4 -> template watch group, 2 -> '=' + expect(countWatches($rootScope)).toEqual(7); // 5 -> template watch group, 2 -> '=' $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:foo;4:'); - expect(countWatches($rootScope)).toEqual(4); + expect(countWatches($rootScope)).toEqual(5); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:foo;4:bar'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.bar = 'baz'; $rootScope.$digest(); @@ -2998,18 +2998,18 @@ describe('$compile', function() { compile('
'); $rootScope.$digest(); expect(element.html()).toBe('1:;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(4); // (4 - 2) -> template watch group, 2 -> {{ }} + expect(countWatches($rootScope)).toEqual(5); // (5 - 2) -> template watch group, 2 -> {{ }} $rootScope.foo = 'foo'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.foo = 'baz'; $rootScope.bar = 'bar'; $rootScope.$digest(); expect(element.html()).toBe('1:foo;2:bar;3:;4:'); - expect(countWatches($rootScope)).toEqual(3); + expect(countWatches($rootScope)).toEqual(4); $rootScope.bar = 'baz'; $rootScope.$digest(); diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index 704932b09085..166fdb705a4b 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -99,11 +99,11 @@ describe('ngBind*', function() { it('should one-time bind the expressions that start with ::', inject(function($rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.name = 'Misko'; - expect($rootScope.$$watchers.length).toEqual(2); + expect($rootScope.$$watchers.length).toEqual(3); $rootScope.$digest(); expect(element.hasClass('ng-binding')).toEqual(true); expect(element.text()).toEqual(' Misko!'); - expect($rootScope.$$watchers.length).toEqual(1); + expect($rootScope.$$watchers.length).toEqual(2); $rootScope.hello = 'Hello'; $rootScope.name = 'Lucas'; $rootScope.$digest(); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index f2f699a86ec7..1c26bed7b676 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -149,14 +149,14 @@ describe('Scope', function() { it('should clean up stable watches from $watchGroup', inject(function($rootScope) { $rootScope.$watchGroup(['::foo', '::bar'], function() {}); - expect($rootScope.$$watchers.length).toEqual(2); + expect($rootScope.$$watchers.length).toEqual(3); $rootScope.$digest(); - expect($rootScope.$$watchers.length).toEqual(2); + expect($rootScope.$$watchers.length).toEqual(3); $rootScope.foo = 'foo'; $rootScope.$digest(); - expect($rootScope.$$watchers.length).toEqual(1); + expect($rootScope.$$watchers.length).toEqual(2); $rootScope.bar = 'bar'; $rootScope.$digest(); @@ -526,6 +526,34 @@ describe('Scope', function() { expect(log).toEqual(['watch1', 'watchAction1', 'watch2', 'watchAction2', 'watch3', 'watchAction3', 'watch2', 'watch3']); })); + + describe('deregisterNotifier', function () { + it('should call the deregisterNotifier when the watch is deregistered', inject( + function($rootScope) { + var notifier = jasmine.createSpy('deregisterNotifier'); + var listenerRemove = $rootScope.$watch('noop', noop, false, notifier); + + expect(notifier).not.toHaveBeenCalled(); + + listenerRemove(); + expect(notifier).toHaveBeenCalledOnce(); + })); + + + it('should call the deregisterNotifier when a one-time expression is stable', inject( + function($rootScope) { + var notifier = jasmine.createSpy('deregisterNotifier'); + $rootScope.$watch('::foo', noop, false, notifier); + + expect(notifier).not.toHaveBeenCalledOnce(); + $rootScope.$digest(); + expect(notifier).not.toHaveBeenCalledOnce(); + + $rootScope.foo = 'foo'; + $rootScope.$digest(); + expect(notifier).toHaveBeenCalledOnce(); + })); + }); });