refactor(change_detection): removed NO_CHANGED and replaced it with WrappedValue

This commit is contained in:
vsavkin 2015-04-23 17:19:29 -07:00 committed by Misko Hevery
parent 4c1e978536
commit e4586249fa
11 changed files with 80 additions and 47 deletions

View File

@ -23,7 +23,7 @@ export {DynamicChangeDetector} from './src/change_detection/dynamic_change_detec
export {ChangeDetectorRef} from './src/change_detection/change_detector_ref'; export {ChangeDetectorRef} from './src/change_detection/change_detector_ref';
export {PipeRegistry} from './src/change_detection/pipes/pipe_registry'; export {PipeRegistry} from './src/change_detection/pipes/pipe_registry';
export {uninitialized} from './src/change_detection/change_detection_util'; export {uninitialized} from './src/change_detection/change_detection_util';
export {NO_CHANGE, Pipe} from './src/change_detection/pipes/pipe'; export {WrappedValue, Pipe} from './src/change_detection/pipes/pipe';
export { export {
defaultPipes, DynamicChangeDetection, JitChangeDetection, defaultPipeRegistry defaultPipes, DynamicChangeDetection, JitChangeDetection, defaultPipeRegistry
} from './src/change_detection/change_detection'; } from './src/change_detection/change_detection';

View File

@ -156,7 +156,8 @@ if (${pipe} === ${UTIL}.unitialized()) {
} }
${newValue} = ${pipe}.transform(${context}); ${newValue} = ${pipe}.transform(${context});
if (! ${UTIL}.noChangeMarker(${newValue})) { if (${oldValue} !== ${newValue}) {
${newValue} = ${UTIL}.unwrapValue(${newValue});
${change} = true; ${change} = true;
${update} ${update}
${addToChanges} ${addToChanges}

View File

@ -2,7 +2,7 @@ import {isPresent, isBlank, BaseException, Type} from 'angular2/src/facade/lang'
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {ProtoRecord} from './proto_record'; import {ProtoRecord} from './proto_record';
import {ExpressionChangedAfterItHasBeenChecked} from './exceptions'; import {ExpressionChangedAfterItHasBeenChecked} from './exceptions';
import {NO_CHANGE} from './pipes/pipe'; import {WrappedValue} from './pipes/pipe';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH} from './constants'; import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH} from './constants';
export var uninitialized = new Object(); export var uninitialized = new Object();
@ -109,8 +109,12 @@ export class ChangeDetectionUtil {
return obj[args[0]]; return obj[args[0]];
} }
static noChangeMarker(value):boolean { static unwrapValue(value:any):any {
return value === NO_CHANGE; if (value instanceof WrappedValue) {
return value.wrapped;
} else {
return value;
}
} }
static throwOnChange(proto:ProtoRecord, change) { static throwOnChange(proto:ProtoRecord, change) {

View File

@ -233,11 +233,13 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
_pipeCheck(proto:ProtoRecord) { _pipeCheck(proto:ProtoRecord) {
var context = this._readContext(proto); var context = this._readContext(proto);
var pipe = this._pipeFor(proto, context); var pipe = this._pipeFor(proto, context);
var prevValue = this._readSelf(proto);
var newValue = pipe.transform(context); var newValue = pipe.transform(context);
if (! ChangeDetectionUtil.noChangeMarker(newValue)) { if (!isSame(prevValue, newValue)) {
var prevValue = this._readSelf(proto); newValue = ChangeDetectionUtil.unwrapValue(newValue);
this._writeSelf(proto, newValue); this._writeSelf(proto, newValue);
this._setChanged(proto, true); this._setChanged(proto, true);

View File

@ -1,6 +1,6 @@
import {Observable, ObservableWrapper} from 'angular2/src/facade/async'; import {Observable, ObservableWrapper} from 'angular2/src/facade/async';
import {isBlank, isPresent} from 'angular2/src/facade/lang'; import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {Pipe, NO_CHANGE} from './pipe'; import {Pipe, WrappedValue} from './pipe';
import {ChangeDetectorRef} from '../change_detector_ref'; import {ChangeDetectorRef} from '../change_detector_ref';
/** /**
@ -67,10 +67,10 @@ export class AsyncPipe extends Pipe {
} }
if (this._latestValue === this._latestReturnedValue) { if (this._latestValue === this._latestReturnedValue) {
return NO_CHANGE; return this._latestReturnedValue;
} else { } else {
this._latestReturnedValue = this._latestValue; this._latestReturnedValue = this._latestValue;
return this._latestValue; return WrappedValue.wrap(this._latestValue);
} }
} }

View File

@ -14,7 +14,7 @@ import {
looseIdentical, looseIdentical,
} from 'angular2/src/facade/lang'; } from 'angular2/src/facade/lang';
import {NO_CHANGE, Pipe} from './pipe'; import {WrappedValue, Pipe} from './pipe';
export class IterableChangesFactory { export class IterableChangesFactory {
supports(obj):boolean { supports(obj):boolean {
@ -117,9 +117,9 @@ export class IterableChanges extends Pipe {
transform(collection){ transform(collection){
if (this.check(collection)) { if (this.check(collection)) {
return this; return WrappedValue.wrap(this);
} else { } else {
return NO_CHANGE; return this;
} }
} }

View File

@ -1,7 +1,7 @@
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {stringify, looseIdentical, isJsObject} from 'angular2/src/facade/lang'; import {stringify, looseIdentical, isJsObject} from 'angular2/src/facade/lang';
import {NO_CHANGE, Pipe} from './pipe'; import {WrappedValue, Pipe} from './pipe';
/** /**
* @exportedAs angular2/pipes * @exportedAs angular2/pipes
@ -54,9 +54,9 @@ export class KeyValueChanges extends Pipe {
transform(map){ transform(map){
if (this.check(map)) { if (this.check(map)) {
return this; return WrappedValue.wrap(this);
} else { } else {
return NO_CHANGE; return this;
} }
} }

View File

@ -1,5 +1,5 @@
import {isBlank} from 'angular2/src/facade/lang'; import {isBlank} from 'angular2/src/facade/lang';
import {Pipe, NO_CHANGE} from './pipe'; import {Pipe, WrappedValue} from './pipe';
/** /**
* @exportedAs angular2/pipes * @exportedAs angular2/pipes
@ -35,9 +35,9 @@ export class NullPipe extends Pipe {
transform(value) { transform(value) {
if (! this.called) { if (! this.called) {
this.called = true; this.called = true;
return null; return WrappedValue.wrap(null);
} else { } else {
return NO_CHANGE; return null;
} }
} }
} }

View File

@ -1,13 +1,33 @@
/** /**
* Indicates that the result of a {@link Pipe} transformation has not changed since the last time the pipe was called. * Indicates that the result of a {@link Pipe} transformation has changed even though the reference has not changed.
* *
* Suppose we have a pipe that computes changes in an array by performing a simple diff operation. If * The wrapped value will be unwrapped by change detection, and the unwrapped value will be stored.
* we call this pipe with the same array twice, it will return `NO_CHANGE` the second time.
* *
* @exportedAs angular2/pipes * @exportedAs angular2/pipes
*/ */
export class WrappedValue {
wrapped:any;
export var NO_CHANGE = new Object(); constructor(wrapped:any) {
this.wrapped = wrapped;
}
static wrap(value:any):WrappedValue {
var w = _wrappedValues[_wrappedIndex++ % 5];
w.wrapped = value;
return w;
}
}
var _wrappedValues = [
new WrappedValue(null),
new WrappedValue(null),
new WrappedValue(null),
new WrappedValue(null),
new WrappedValue(null)
];
var _wrappedIndex = 0;
/** /**
* An interface for extending the list of pipes known to Angular. * An interface for extending the list of pipes known to Angular.

View File

@ -8,7 +8,7 @@ import {Lexer} from 'angular2/src/change_detection/parser/lexer';
import {Locals} from 'angular2/src/change_detection/parser/locals'; import {Locals} from 'angular2/src/change_detection/parser/locals';
import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, BindingRecord, DirectiveRecord, DirectiveIndex, import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, BindingRecord, DirectiveRecord, DirectiveIndex,
PipeRegistry, Pipe, NO_CHANGE, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH, DEFAULT} from 'angular2/change_detection'; PipeRegistry, Pipe, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH, DEFAULT, WrappedValue} from 'angular2/change_detection';
import {JitProtoChangeDetector, DynamicProtoChangeDetector} from 'angular2/src/change_detection/proto_change_detector'; import {JitProtoChangeDetector, DynamicProtoChangeDetector} from 'angular2/src/change_detection/proto_change_detector';
@ -733,7 +733,7 @@ export function main() {
}); });
}); });
it("should do nothing when returns NO_CHANGE", () => { it("should do nothing when no change", () => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe()) var registry = new FakePipeRegistry('pipe', () => new IdentityPipe())
var ctx = new Person("Megatron"); var ctx = new Person("Megatron");
@ -741,16 +741,27 @@ export function main() {
var cd = c["changeDetector"]; var cd = c["changeDetector"];
var dispatcher = c["dispatcher"]; var dispatcher = c["dispatcher"];
cd.detectChanges();
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron']); expect(dispatcher.log).toEqual(['memo=Megatron']);
ctx.name = "Optimus Prime";
dispatcher.clear(); dispatcher.clear();
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Optimus Prime']); expect(dispatcher.log).toEqual([]);
});
it("should unwrap the wrapped value", () => {
var registry = new FakePipeRegistry('pipe', () => new WrappedPipe())
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, null, registry);
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.detectChanges();
expect(dispatcher.log).toEqual(['memo=Megatron']);
}); });
}); });
}); });
@ -798,19 +809,14 @@ class OncePipe extends Pipe {
} }
class IdentityPipe extends Pipe { class IdentityPipe extends Pipe {
state:any;
supports(newValue) {
return true;
}
transform(value) { transform(value) {
if (this.state === value) { return value;
return NO_CHANGE; }
} else { }
this.state = value;
return value; class WrappedPipe extends Pipe {
} transform(value) {
return WrappedValue.wrap(value);
} }
} }

View File

@ -2,8 +2,8 @@ import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach,
AsyncTestCompleter, inject, proxy, SpyObject} from 'angular2/test_lib'; AsyncTestCompleter, inject, proxy, SpyObject} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang'; import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {WrappedValue} from 'angular2/src/change_detection/pipes/pipe';
import {AsyncPipe} from 'angular2/src/change_detection/pipes/async_pipe'; import {AsyncPipe} from 'angular2/src/change_detection/pipes/async_pipe';
import {NO_CHANGE} from 'angular2/src/change_detection/pipes/pipe';
import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref'; import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref';
import {EventEmitter, Observable, ObservableWrapper, PromiseWrapper} from 'angular2/src/facade/async'; import {EventEmitter, Observable, ObservableWrapper, PromiseWrapper} from 'angular2/src/facade/async';
@ -36,25 +36,25 @@ export function main() {
expect(pipe.transform(emitter)).toBe(null); expect(pipe.transform(emitter)).toBe(null);
}); });
it("should return the latest available value", inject([AsyncTestCompleter], (async) => { it("should return the latest available value wrapped", inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter); pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message); ObservableWrapper.callNext(emitter, message);
PromiseWrapper.setTimeout(() => { PromiseWrapper.setTimeout(() => {
expect(pipe.transform(emitter)).toEqual(message); expect(pipe.transform(emitter)).toEqual(new WrappedValue(message));
async.done(); async.done();
}, 0) }, 0)
})); }));
it("should return NO_CHANGE when nothing has changed since the last call", it("should return same value when nothing has changed since the last call",
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter); pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message); ObservableWrapper.callNext(emitter, message);
PromiseWrapper.setTimeout(() => { PromiseWrapper.setTimeout(() => {
pipe.transform(emitter); pipe.transform(emitter);
expect(pipe.transform(emitter)).toBe(NO_CHANGE); expect(pipe.transform(emitter)).toBe(message);
async.done(); async.done();
}, 0) }, 0)
})); }));
@ -66,11 +66,11 @@ export function main() {
var newEmitter = new EventEmitter(); var newEmitter = new EventEmitter();
expect(pipe.transform(newEmitter)).toBe(null); expect(pipe.transform(newEmitter)).toBe(null);
// this should not affect the pipe, so it should return NO_CHANGE // this should not affect the pipe
ObservableWrapper.callNext(emitter, message); ObservableWrapper.callNext(emitter, message);
PromiseWrapper.setTimeout(() => { PromiseWrapper.setTimeout(() => {
expect(pipe.transform(newEmitter)).toBe(NO_CHANGE); expect(pipe.transform(newEmitter)).toBe(null);
async.done(); async.done();
}, 0) }, 0)
})); }));