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:
vsavkin
2016-01-05 13:42:21 -08:00
committed by Rado Kirov
parent db87baeb98
commit 42231f5719
10 changed files with 141 additions and 12 deletions

View File

@ -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});
}
`;

View File

@ -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);
}
}
}

View File

@ -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) {