From 23a080026a2e07eca5666dffc41bfdc6950217af Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sun, 1 Feb 2015 12:09:35 -0800 Subject: [PATCH] feat(change_detection): add mode to ChangeDetector --- .../src/abstract_change_detector.js | 17 +++- .../src/binding_propagation_config.js | 88 +++++++++++++++++++ modules/change_detection/src/interfaces.js | 26 ++++++ .../test/change_detection_spec.js | 70 +++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 modules/change_detection/src/binding_propagation_config.js diff --git a/modules/change_detection/src/abstract_change_detector.js b/modules/change_detection/src/abstract_change_detector.js index 10f00b5ca0..3073463dae 100644 --- a/modules/change_detection/src/abstract_change_detector.js +++ b/modules/change_detection/src/abstract_change_detector.js @@ -1,12 +1,15 @@ +import {isPresent} from 'facade/src/lang'; 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 { children:List; parent:ChangeDetector; + status:string; constructor() { this.children = []; + this.status = CHECK_ALWAYS; } addChild(cd:ChangeDetector) { @@ -31,8 +34,20 @@ export class AbstractChangeDetector extends ChangeDetector { } _detectChanges(throwOnChange:boolean) { + if (this.mode === DETACHED || this.mode === CHECKED) return; + this.detectChangesInRecords(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){} diff --git a/modules/change_detection/src/binding_propagation_config.js b/modules/change_detection/src/binding_propagation_config.js new file mode 100644 index 0000000000..2b2f087e37 --- /dev/null +++ b/modules/change_detection/src/binding_propagation_config.js @@ -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):List { + 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){ + 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; +} \ No newline at end of file diff --git a/modules/change_detection/src/interfaces.js b/modules/change_detection/src/interfaces.js index a824f2e363..25c79237c8 100644 --- a/modules/change_detection/src/interfaces.js +++ b/modules/change_detection/src/interfaces.js @@ -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 { onRecordChange(groupMemento, records:List) {} } export class ChangeDetector { parent:ChangeDetector; + mode:string; addChild(cd:ChangeDetector) {} removeChild(cd:ChangeDetector) {} diff --git a/modules/change_detection/test/change_detection_spec.js b/modules/change_detection/test/change_detection_spec.js index cd9eb10155..03adc37ce7 100644 --- a/modules/change_detection/test/change_detection_spec.js +++ b/modules/change_detection/test/change_detection_spec.js @@ -13,6 +13,7 @@ import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, ContextWi 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() { @@ -473,6 +474,75 @@ export function main() { 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); + }); + }); }); }); }