feat(change_detection): allow all legal programs in the dev mode
BEFORE:
The following would throw in the dev mode because `f` would return a new array when called by checkNoChanges.
@Component({
template: `
{{f()}}
`
})
class A {
f() { return [1]; }
}
AFTER:
The checkNoChanges function compares only primitives types for equality, and deeply compares iterables. Other objects cannot cause checkNoChanges to throw. This means that the dev mode would never fail given a legal program, but may allow some illegal programs.
This commit is contained in:
@@ -350,6 +350,7 @@ export class ChangeDetectorJITGenerator {
|
||||
var condition = `!${pipe}.pure || (${contexOrArgCheck.join(" || ")})`;
|
||||
|
||||
var check = `
|
||||
${this._genThrowOnChangeCheck(oldValue, newValue)}
|
||||
if (${this.changeDetectionUtilVarName}.looseNotIdentical(${oldValue}, ${newValue})) {
|
||||
${newValue} = ${this.changeDetectionUtilVarName}.unwrapValue(${newValue})
|
||||
${this._genChangeMarker(r)}
|
||||
@@ -377,6 +378,7 @@ export class ChangeDetectorJITGenerator {
|
||||
`;
|
||||
|
||||
var check = `
|
||||
${this._genThrowOnChangeCheck(oldValue, newValue)}
|
||||
if (${this.changeDetectionUtilVarName}.looseNotIdentical(${oldValue}, ${newValue})) {
|
||||
${this._genChangeMarker(r)}
|
||||
${this._genUpdateDirectiveOrElement(r)}
|
||||
@@ -409,7 +411,6 @@ export class ChangeDetectorJITGenerator {
|
||||
if (!r.lastInBinding) return "";
|
||||
|
||||
var newValue = this._names.getLocalName(r.selfIndex);
|
||||
var oldValue = this._names.getFieldName(r.selfIndex);
|
||||
var notifyDebug = this.genConfig.logBindingUpdate ? `this.logBindingUpdate(${newValue});` : "";
|
||||
|
||||
var br = r.bindingRecord;
|
||||
@@ -417,14 +418,12 @@ export class ChangeDetectorJITGenerator {
|
||||
var directiveProperty =
|
||||
`${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.${br.target.name}`;
|
||||
return `
|
||||
${this._genThrowOnChangeCheck(oldValue, newValue)}
|
||||
${directiveProperty} = ${newValue};
|
||||
${notifyDebug}
|
||||
${IS_CHANGED_LOCAL} = true;
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
${this._genThrowOnChangeCheck(oldValue, newValue)}
|
||||
this.notifyDispatcher(${newValue});
|
||||
${notifyDebug}
|
||||
`;
|
||||
@@ -435,7 +434,7 @@ export class ChangeDetectorJITGenerator {
|
||||
_genThrowOnChangeCheck(oldValue: string, newValue: string): string {
|
||||
if (assertionsEnabled()) {
|
||||
return `
|
||||
if(throwOnChange) {
|
||||
if (throwOnChange && !${this.changeDetectionUtilVarName}.devModeEqual(${oldValue}, ${newValue})) {
|
||||
this.throwOnChangeError(${oldValue}, ${newValue});
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,10 +4,17 @@ import {
|
||||
isBlank,
|
||||
Type,
|
||||
StringWrapper,
|
||||
looseIdentical
|
||||
looseIdentical,
|
||||
isPrimitive
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
ListWrapper,
|
||||
MapWrapper,
|
||||
StringMapWrapper,
|
||||
isListLikeIterable,
|
||||
areIterablesEqual
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {ProtoRecord} from './proto_record';
|
||||
import {ChangeDetectionStrategy, isDefaultChangeDetectionStrategy} from './constants';
|
||||
import {implementsOnDestroy} from './pipe_lifecycle_reflector';
|
||||
@@ -214,4 +221,17 @@ export class ChangeDetectionUtil {
|
||||
}
|
||||
|
||||
static looseNotIdentical(a: any, b: any): boolean { return !looseIdentical(a, b); }
|
||||
|
||||
static devModeEqual(a: any, b: any): boolean {
|
||||
if (isListLikeIterable(a) && isListLikeIterable(b)) {
|
||||
return areIterablesEqual(a, b, ChangeDetectionUtil.devModeEqual);
|
||||
|
||||
} else if (!isListLikeIterable(a) && !isPrimitive(a) && !isListLikeIterable(b) &&
|
||||
!isPrimitive(b)) {
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return looseIdentical(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +304,10 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
|
||||
|
||||
if (proto.shouldBeChecked()) {
|
||||
var prevValue = this._readSelf(proto, values);
|
||||
if (ChangeDetectionUtil.looseNotIdentical(prevValue, currValue)) {
|
||||
var detectedChange = throwOnChange ?
|
||||
!ChangeDetectionUtil.devModeEqual(prevValue, currValue) :
|
||||
ChangeDetectionUtil.looseNotIdentical(prevValue, currValue);
|
||||
if (detectedChange) {
|
||||
if (proto.lastInBinding) {
|
||||
var change = ChangeDetectionUtil.simpleChange(prevValue, currValue);
|
||||
if (throwOnChange) this.throwOnChangeError(prevValue, currValue);
|
||||
@@ -405,7 +408,10 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
|
||||
|
||||
if (proto.shouldBeChecked()) {
|
||||
var prevValue = this._readSelf(proto, values);
|
||||
if (ChangeDetectionUtil.looseNotIdentical(prevValue, currValue)) {
|
||||
var detectedChange = throwOnChange ?
|
||||
!ChangeDetectionUtil.devModeEqual(prevValue, currValue) :
|
||||
ChangeDetectionUtil.looseNotIdentical(prevValue, currValue);
|
||||
if (detectedChange) {
|
||||
currValue = ChangeDetectionUtil.unwrapValue(currValue);
|
||||
|
||||
if (proto.lastInBinding) {
|
||||
|
||||
@@ -225,6 +225,19 @@ class ListWrapper {
|
||||
|
||||
bool isListLikeIterable(obj) => obj is Iterable;
|
||||
|
||||
bool areIterablesEqual(Iterable a, Iterable b, Function comparator) {
|
||||
var iterator1 = a.iterator;
|
||||
var iterator2 = b.iterator;
|
||||
|
||||
while (true) {
|
||||
var done1 = !iterator1.moveNext();
|
||||
var done2 = !iterator2.moveNext();
|
||||
if (done1 && done2) return true;
|
||||
if (done1 || done2) return false;
|
||||
if (!comparator(iterator2.current, iterator2.current)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
void iterateListLike(iter, fn(item)) {
|
||||
assert(iter is Iterable);
|
||||
for (var item in iter) {
|
||||
|
||||
@@ -274,6 +274,19 @@ export function isListLikeIterable(obj: any): boolean {
|
||||
getSymbolIterator() in obj); // JS Iterable have a Symbol.iterator prop
|
||||
}
|
||||
|
||||
export function areIterablesEqual(a: any, b: any, comparator: Function): boolean {
|
||||
var iterator1 = a[getSymbolIterator()]();
|
||||
var iterator2 = b[getSymbolIterator()]();
|
||||
|
||||
while (true) {
|
||||
let item1 = iterator1.next();
|
||||
let item2 = iterator2.next();
|
||||
if (item1.done && item2.done) return true;
|
||||
if (item1.done || item2.done) return false;
|
||||
if (!comparator(item1.value, item2.value)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function iterateListLike(obj: any, fn: Function) {
|
||||
if (isArray(obj)) {
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
|
||||
@@ -345,6 +345,8 @@ class DateWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
bool isPrimitive(Object obj) => obj is num || obj is bool || obj == null || obj is String;
|
||||
|
||||
// needed to match the exports from lang.js
|
||||
var global = null;
|
||||
|
||||
|
||||
@@ -427,3 +427,7 @@ export function evalExpression(sourceUrl: string, expr: string, declarations: st
|
||||
}
|
||||
return new Function(...fnArgNames.concat(fnBody))(...fnArgValues);
|
||||
}
|
||||
|
||||
export function isPrimitive(obj: any): boolean {
|
||||
return !isJsObject(obj);
|
||||
}
|
||||
Reference in New Issue
Block a user