feat(change_detection): add mode to ChangeDetector

This commit is contained in:
vsavkin 2015-02-01 12:09:35 -08:00 committed by Alex Eagle
parent 3067601961
commit 23a080026a
4 changed files with 200 additions and 1 deletions

View File

@ -1,12 +1,15 @@
import {isPresent} from 'facade/src/lang';
import {List, ListWrapper} from 'facade/src/collection'; import {List, ListWrapper} from 'facade/src/collection';
import {ChangeDetector} from './interfaces'; import {ChangeDetector, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './interfaces';
export class AbstractChangeDetector extends ChangeDetector { export class AbstractChangeDetector extends ChangeDetector {
children:List; children:List;
parent:ChangeDetector; parent:ChangeDetector;
status:string;
constructor() { constructor() {
this.children = []; this.children = [];
this.status = CHECK_ALWAYS;
} }
addChild(cd:ChangeDetector) { addChild(cd:ChangeDetector) {
@ -31,8 +34,20 @@ export class AbstractChangeDetector extends ChangeDetector {
} }
_detectChanges(throwOnChange:boolean) { _detectChanges(throwOnChange:boolean) {
if (this.mode === DETACHED || this.mode === CHECKED) return;
this.detectChangesInRecords(throwOnChange); this.detectChangesInRecords(throwOnChange);
this._detectChangesInChildren(throwOnChange); this._detectChangesInChildren(throwOnChange);
if (this.mode === CHECK_ONCE) this.mode = CHECKED;
}
markAsCheckOnce(){
var c = this;
while(isPresent(c) && (c.mode === CHECKED || c.mode === CHECK_ALWAYS)) {
if (c.mode === CHECKED) c.mode = CHECK_ONCE;
c = c.parent;
}
} }
detectChangesInRecords(throwOnChange:boolean){} detectChangesInRecords(throwOnChange:boolean){}

View File

@ -0,0 +1,88 @@
import {isPresent} from 'facade/lang';
import {List, ListWrapper, Map, MapWrapper} from 'facade/collection';
import {RECORD_TYPE_SELF, ProtoRecord} from './proto_change_detector';
/**
* Removes "duplicate" records. It assuming that record evaluation does not
* have side-effects.
*
* Records that are not last in bindings are removed and all the indices
* of the records that depend on them are updated.
*
* Records that are last in bindings CANNOT be removed, and instead are
* replaced with very cheap SELF records.
*/
export function coalesce(records:List<ProtoRecord>):List<ProtoRecord> {
var res = ListWrapper.create();
var indexMap = MapWrapper.create();
for (var i = 0; i < records.length; ++i) {
var r = records[i];
var record = _replaceIndices(r, res.length + 1, indexMap);
var matchingRecord = _findMatching(record, res);
if (isPresent(matchingRecord) && record.lastInBinding) {
ListWrapper.push(res, _selfRecord(record, matchingRecord.selfIndex, res.length + 1));
MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex);
} else if (isPresent(matchingRecord) && !record.lastInBinding) {
MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex);
} else {
ListWrapper.push(res, record);
MapWrapper.set(indexMap, r.selfIndex, record.selfIndex);
}
}
return res;
}
function _selfRecord(r:ProtoRecord, contextIndex:number, selfIndex:number):ProtoRecord {
return new ProtoRecord(
RECORD_TYPE_SELF,
"self",
null,
[],
r.fixedArgs,
contextIndex,
selfIndex,
r.bindingMemento,
r.groupMemento,
r.expressionAsString,
r.lastInBinding,
r.lastInGroup
);
}
function _findMatching(r:ProtoRecord, rs:List<ProtoRecord>){
return ListWrapper.find(rs, (rr) =>
rr.mode === r.mode &&
rr.funcOrValue === r.funcOrValue &&
rr.contextIndex === r.contextIndex &&
ListWrapper.equals(rr.args, r.args)
);
}
function _replaceIndices(r:ProtoRecord, selfIndex:number, indexMap:Map) {
var args = ListWrapper.map(r.args, (a) => _map(indexMap, a));
var contextIndex = _map(indexMap, r.contextIndex);
return new ProtoRecord(
r.mode,
r.name,
r.funcOrValue,
args,
r.fixedArgs,
contextIndex,
selfIndex,
r.bindingMemento,
r.groupMemento,
r.expressionAsString,
r.lastInBinding,
r.lastInGroup
);
}
function _map(indexMap:Map, value:number) {
var r = MapWrapper.get(indexMap, value)
return isPresent(r) ? r : value;
}

View File

@ -19,12 +19,38 @@ export class ChangeRecord {
} }
} }
/**
* CHECK_ONCE means that after calling detectChanges the mode of the change detector
* will become CHECKED.
*/
export const CHECK_ONCE="CHECK_ONCE";
/**
* CHECKED means that the change detector should be skipped until its mode changes to
* CHECK_ONCE or CHECK_ALWAYS.
*/
export const CHECKED="CHECKED";
/**
* CHECK_ALWAYS means that after calling detectChanges the mode of the change detector
* will remain CHECK_ALWAYS.
*/
export const CHECK_ALWAYS="ALWAYS_CHECK";
/**
* DETACHED means that the change detector sub tree is not a part of the main tree and
* should be skipped.
*/
export const DETACHED="DETACHED";
export class ChangeDispatcher { export class ChangeDispatcher {
onRecordChange(groupMemento, records:List<ChangeRecord>) {} onRecordChange(groupMemento, records:List<ChangeRecord>) {}
} }
export class ChangeDetector { export class ChangeDetector {
parent:ChangeDetector; parent:ChangeDetector;
mode:string;
addChild(cd:ChangeDetector) {} addChild(cd:ChangeDetector) {}
removeChild(cd:ChangeDetector) {} removeChild(cd:ChangeDetector) {}

View File

@ -13,6 +13,7 @@ import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, ContextWi
import {JitProtoChangeDetector, DynamicProtoChangeDetector} from 'change_detection/src/proto_change_detector'; import {JitProtoChangeDetector, DynamicProtoChangeDetector} from 'change_detection/src/proto_change_detector';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from 'change_detection/src/interfaces';
export function main() { export function main() {
@ -473,6 +474,75 @@ export function main() {
expect(count).toEqual(1); expect(count).toEqual(1);
}); });
}); });
describe("mode", () => {
it("should not check a detached change detector", () => {
var c = createChangeDetector('name', 'a', new TestData("value"));
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.mode = DETACHED;
cd.detectChanges();
expect(dispatcher.log).toEqual([]);
});
it("should not check a checked change detector", () => {
var c = createChangeDetector('name', 'a', new TestData("value"));
var cd = c["changeDetector"];
var dispatcher = c["dispatcher"];
cd.mode = CHECKED;
cd.detectChanges();
expect(dispatcher.log).toEqual([]);
});
it("should change CHECK_ONCE to CHECKED", () => {
var cd = createProtoChangeDetector().instantiate(null, null);
cd.mode = CHECK_ONCE;
cd.detectChanges();
expect(cd.mode).toEqual(CHECKED);
});
it("should not change the CHECK_ALWAYS", () => {
var cd = createProtoChangeDetector().instantiate(null, null);
cd.mode = CHECK_ALWAYS;
cd.detectChanges();
expect(cd.mode).toEqual(CHECK_ALWAYS);
});
});
describe("markAsCheckOnce", () => {
function changeDetector(mode, parent) {
var cd = createProtoChangeDetector().instantiate(null, null);
cd.mode = mode;
if (isPresent(parent)) parent.addChild(cd);
return cd;
}
it("should mark all checked detectors as CHECK_ONCE " +
"until reaching a detached one", () => {
var root = changeDetector(CHECK_ALWAYS, null);
var disabled = changeDetector(DETACHED, root);
var parent = changeDetector(CHECKED, disabled);
var child = changeDetector(CHECK_ALWAYS, parent);
var grandChild = changeDetector(CHECKED, child);
grandChild.markAsCheckOnce();
expect(root.mode).toEqual(CHECK_ALWAYS);
expect(disabled.mode).toEqual(DETACHED);
expect(parent.mode).toEqual(CHECK_ONCE);
expect(child.mode).toEqual(CHECK_ALWAYS);
expect(grandChild.mode).toEqual(CHECK_ONCE);
});
});
}); });
}); });
} }