feat(change_detection): add mode to ChangeDetector
This commit is contained in:
parent
3067601961
commit
23a080026a
|
@ -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){}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue