Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 29f9e26

Browse files
CodierIgorMinar
authored andcommittedNov 21, 2011
fix(scope): $watch (and angular.equals) should support NaN values
- since NaN !== NaN in javascript digest can get into an infinite loop when model value is set to NaN - angular.equals(NaN, NaN) should return true since that's what we expect when comparing primitives or objects containing NaN values Previously NaN because of its special === properties was used as the initial value for watches, but that results in issues when NaN is used as model value. In order to allow for model to be anything incuding undefined and NaN we need to mark the initial value differently in a way that would avoid these issues, allow us to run digest without major perf penalties and allow for clients to determine if the listener is being called because the watcher is being initialized or because the model changed. This implementation covers all of these scenarios. BREAKING CHANGE: previously to detect if the listener was called because the watcher was being initialized, it was suggested that clients check if old value is NaN. With this change, the check should be if the newVal equals the oldVal. Closes #657
1 parent 8d19448 commit 29f9e26

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed
 

‎src/Angular.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ function copy(source, destination){
624624
*
625625
* * Both objects or values pass `===` comparison.
626626
* * Both objects or values are of the same type and all of their properties pass `===` comparison.
627+
* * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal)
627628
*
628629
* During a property comparision, properties of `function` type and properties with names
629630
* that begin with `$` are ignored.
@@ -634,11 +635,11 @@ function copy(source, destination){
634635
* @param {*} o1 Object or value to compare.
635636
* @param {*} o2 Object or value to compare.
636637
* @returns {boolean} True if arguments are equal.
637-
*
638638
*/
639639
function equals(o1, o2) {
640640
if (o1 === o2) return true;
641641
if (o1 === null || o2 === null) return false;
642+
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
642643
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
643644
if (t1 == t2 && t1 == 'object') {
644645
if (isArray(o1)) {

‎src/directives.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,9 @@ function ngClass(selector) {
576576
return function(element) {
577577
this.$watch(expression, function(scope, newVal, oldVal) {
578578
if (selector(scope.$index)) {
579-
if (oldVal) element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
579+
if (oldVal && (newVal !== oldVal)) {
580+
element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
581+
}
580582
if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);
581583
}
582584
});
@@ -823,7 +825,9 @@ angularDirective("ng:hide", function(expression, element){
823825
angularDirective("ng:style", function(expression, element) {
824826
return function(element) {
825827
this.$watch(expression, function(scope, newStyles, oldStyles) {
826-
if (oldStyles) forEach(oldStyles, function(val, style) { element.css(style, '');});
828+
if (oldStyles && (newStyles !== oldStyles)) {
829+
forEach(oldStyles, function(val, style) { element.css(style, '');});
830+
}
827831
if (newStyles) element.css(newStyles);
828832
});
829833
};

‎src/service/scope.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ function $RootScopeProvider(){
187187
* reruns when it detects changes the `watchExpression` can execute multiple times per
188188
* {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and should be idempotent.)
189189
* - The `listener` is called only when the value from the current `watchExpression` and the
190-
* previous call to `watchExpression' are not equal. The inequality is determined according to
190+
* previous call to `watchExpression' are not equal (with the exception of the initial run
191+
* see below). The inequality is determined according to
191192
* {@link angular.equals} function. To save the value of the object for later comparison
192193
* {@link angular.copy} function is used. It also means that watching complex options will
193194
* have adverse memory and performance implications.
@@ -201,6 +202,13 @@ function $RootScopeProvider(){
201202
* can execute multiple times per {@link angular.module.ng.$rootScope.Scope#$digest $digest} cycle when a change is
202203
* detected, be prepared for multiple calls to your listener.)
203204
*
205+
* After a watcher is registered with the scope, the `listener` fn is called asynchronously
206+
* (via {@link angular.module.ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the
207+
* watcher. In rare cases, this is undesirable because the listener is called when the result
208+
* of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you
209+
* can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the
210+
* listener was called due to initialization.
211+
*
204212
*
205213
* # Example
206214
<pre>
@@ -244,7 +252,7 @@ function $RootScopeProvider(){
244252
array = scope.$$watchers,
245253
watcher = {
246254
fn: listenFn,
247-
last: Number.NaN, // NaN !== NaN. We used this to force $watch to fire on first run.
255+
last: initWatchVal,
248256
get: get,
249257
exp: watchExp
250258
};
@@ -345,7 +353,7 @@ function $RootScopeProvider(){
345353
if ((value = watch.get(current)) !== (last = watch.last) && !equals(value, last)) {
346354
dirty = true;
347355
watch.last = copy(value);
348-
watch.fn(current, value, last);
356+
watch.fn(current, value, ((last === initWatchVal) ? value : last));
349357
if (ttl < 5) {
350358
if (!watchLog[4-ttl]) watchLog[4-ttl] = [];
351359
if (isFunction(watch.exp)) {
@@ -669,6 +677,10 @@ function $RootScopeProvider(){
669677
return fn;
670678
}
671679

672-
680+
/**
681+
* function used as an initial value for watchers.
682+
* because it's uniqueue we can easily tell it apart from other values
683+
*/
684+
function initWatchVal() {}
673685
}];
674686
}

‎test/AngularSpec.js

+4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ describe('angular', function() {
110110

111111
expect(equals(undefined, undefined)).toBe(true);
112112
});
113+
114+
it('should treat two NaNs as equal', function() {
115+
expect(equals(NaN, NaN)).toBe(true);
116+
});
113117
});
114118

115119
describe('size', function() {

‎test/service/scopeSpec.js

+33
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,39 @@ describe('Scope', function() {
305305
root.$digest(); //trigger
306306
expect(listener).not.toHaveBeenCalled();
307307
}));
308+
309+
310+
it('should not infinitely digest when current value is NaN', inject(function($rootScope) {
311+
$rootScope.$watch(function() { return NaN;});
312+
313+
expect(function() {
314+
$rootScope.$digest();
315+
}).not.toThrow();
316+
}));
317+
318+
319+
it('should always call the watchr with newVal and oldVal equal on the first run',
320+
inject(function($rootScope) {
321+
var log = [];
322+
function logger(scope, newVal, oldVal) {
323+
var val = (newVal === oldVal || (newVal !== oldVal && oldVal !== newVal)) ? newVal : 'xxx';
324+
log.push(val);
325+
};
326+
327+
$rootScope.$watch(function() { return NaN;}, logger);
328+
$rootScope.$watch(function() { return undefined;}, logger);
329+
$rootScope.$watch(function() { return '';}, logger);
330+
$rootScope.$watch(function() { return false;}, logger);
331+
$rootScope.$watch(function() { return {};}, logger);
332+
$rootScope.$watch(function() { return 23;}, logger);
333+
334+
$rootScope.$digest();
335+
expect(isNaN(log.shift())).toBe(true); //jasmine's toBe and toEqual don't work well with NaNs
336+
expect(log).toEqual([undefined, '', false, {}, 23]);
337+
log = []
338+
$rootScope.$digest();
339+
expect(log).toEqual([]);
340+
}));
308341
});
309342

310343

0 commit comments

Comments
 (0)
This repository has been archived.