From daf8f72b74ff1c299fd456b51224a8924d2ed713 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 14 Nov 2014 15:35:41 -0800 Subject: [PATCH] feat(ChangeDetector): implement enabling/disabling records --- .../change_detection/src/change_detector.js | 9 +- modules/change_detection/src/record.js | 13 ++ modules/change_detection/src/watch_group.js | 79 +++++++++-- .../test/change_detector_spec.js | 37 ++++- .../change_detection/test/watch_group_spec.js | 126 ++++++++++++++++++ 5 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 modules/change_detection/test/watch_group_spec.js diff --git a/modules/change_detection/src/change_detector.js b/modules/change_detection/src/change_detector.js index 014e468843..1ed3a903e9 100644 --- a/modules/change_detection/src/change_detector.js +++ b/modules/change_detection/src/change_detector.js @@ -1,6 +1,6 @@ import {ProtoWatchGroup, WatchGroup} from './watch_group'; import {ProtoRecord, Record} from './record'; -import {FIELD, int} from 'facade/lang'; +import {FIELD, int, isPresent} from 'facade/lang'; export * from './record'; export * from './watch_group' @@ -12,11 +12,10 @@ export class ChangeDetector { } detectChanges():int { - var record:Record = this._rootWatchGroup.headRecord; var count:int = 0; - for (record = this._rootWatchGroup.headRecord; - record != null; - record = record.next) { + for (var record = this._rootWatchGroup.headEnabledRecord; + isPresent(record); + record = record.nextEnabled) { if (record.check()) { count++; } diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index 5eb1723597..8069d2e2b1 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -65,6 +65,12 @@ export class Record { @FIELD('final protoRecord:ProtoRecord') @FIELD('next:Record') @FIELD('prev:Record') + + /// This reference can change. + @FIELD('nextEnabled:Record') + + /// This reference can change. + @FIELD('prevEnabled:Record') @FIELD('dest:Record') @FIELD('previousValue') @@ -86,6 +92,9 @@ export class Record { this.next = null; this.prev = null; + this.nextEnabled = null; + this.prevEnabled = null; + this.disabled = false; this.dest = null; this.previousValue = null; @@ -163,9 +172,11 @@ export class Record { return FunctionWrapper.apply(this.context, this.args); case MODE_STATE_INVOKE_PURE_FUNCTION: + this.watchGroup.disableRecord(this); return FunctionWrapper.apply(this.funcOrValue, this.args); case MODE_STATE_CONST: + this.watchGroup.disableRecord(this); return this.funcOrValue; case MODE_STATE_MARKER: @@ -184,10 +195,12 @@ export class Record { updateArg(value, position:int) { this.args[position] = value; + this.watchGroup.enableRecord(this); } updateContext(value) { this.context = value; + this.watchGroup.enableRecord(this); } } diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index f8b93ad8f4..84ff7411dc 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -56,21 +56,11 @@ export class ProtoWatchGroup { } _createRecords(watchGroup:WatchGroup, formatters:Map) { - var tail, prevRecord; - watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord, formatters); - this.headRecord.recordInConstruction = watchGroup.headRecord; - - for (var proto = this.headRecord.next; proto != null; proto = proto.next) { - prevRecord = tail; - - tail = new Record(watchGroup, proto, formatters); - proto.recordInConstruction = tail; - - tail.prev = prevRecord; - prevRecord.next = tail; + for (var proto = this.headRecord; proto != null; proto = proto.next) { + var record = new Record(watchGroup, proto, formatters); + proto.recordInConstruction = record; + watchGroup.addRecord(record); } - - watchGroup.tailRecord = tail; } _setDestination() { @@ -95,9 +85,70 @@ export class WatchGroup { this.dispatcher = dispatcher; this.headRecord = null; this.tailRecord = null; + this.headEnabledRecord = null; + this.tailEnabledRecord = null; this.context = null; } + addRecord(record:Record) { + if (isPresent(this.tailRecord)) { + this.tailRecord.next = record; + this.tailRecord.nextEnabled = record; + record.prev = this.tailRecord; + record.prevEnabled = this.tailRecord; + this.tailRecord = this.tailEnabledRecord = record; + + } else { + this.headRecord = this.tailRecord = record; + this.headEnabledRecord = this.tailEnabledRecord = record; + } + } + + disableRecord(record:Record) { + var prev = record.prevEnabled; + var next = record.nextEnabled; + + record.disabled = true; + + if (isPresent(prev)) { + prev.nextEnabled = next; + } else { + this.headEnabledRecord = next; + } + + if (isPresent(next)) { + next.prevEnabled = prev; + } else { + this.tailEnabledRecord = prev; + } + } + + enableRecord(record:Record) { + if (!record.disabled) return; + + var prev = record.prev; + while (prev != null && prev.disabled) prev = prev.prev; + + var next = record.next; + while (next != null && next.disabled) next = next.next; + + record.disabled = false; + record.prevEnabled = prev; + record.nextEnabled = next; + + if (isPresent(prev)) { + prev.nextEnabled = record; + } else { + this.headEnabledRecord = record; + } + + if (isPresent(next)) { + next.prevEnabled = record; + } else { + this.tailEnabledRecord = record; + } + } + insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) { throw 'not implemented'; } diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index 3677949d98..656406df0b 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -152,13 +152,36 @@ export function main() { expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']); }); - it("should support formatters", () => { - var formatters = MapWrapper.createFromPairs([ - ["uppercase", (v) => v.toUpperCase()], - ["wrap", (v, before, after) => `${before}${v}${after}`] - ]); - expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']); - expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']); + describe("formatters", () => { + it("should support formatters", () => { + var formatters = MapWrapper.createFromPairs([ + ['uppercase', (v) => v.toUpperCase()], + ['wrap', (v, before, after) => `${before}${v}${after}`]]); + expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']); + expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']); + }); + + it("should rerun formatters only when arguments change", () => { + var counter = 0; + var formatters = MapWrapper.createFromPairs([ + ['formatter', (_) => {counter += 1; return 'value'}] + ]); + + var person = new Person('Jim'); + + var c = createChangeDetector('formatter', 'name | formatter', person, formatters); + var cd = c['changeDetector']; + + cd.detectChanges(); + expect(counter).toEqual(1); + + cd.detectChanges(); + expect(counter).toEqual(1); + + person.name = 'bob'; + cd.detectChanges(); + expect(counter).toEqual(2); + }); }); }); }); diff --git a/modules/change_detection/test/watch_group_spec.js b/modules/change_detection/test/watch_group_spec.js new file mode 100644 index 0000000000..b189f4904b --- /dev/null +++ b/modules/change_detection/test/watch_group_spec.js @@ -0,0 +1,126 @@ +import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib'; + +import {List, ListWrapper, MapWrapper} from 'facade/collection'; +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; +import {ClosureMap} from 'change_detection/parser/closure_map'; + +import { + ChangeDetector, + ProtoWatchGroup, + WatchGroup, + WatchGroupDispatcher, + ProtoRecord +} from 'change_detection/change_detector'; + +import {Record} from 'change_detection/record'; + +export function main() { + function createRecord(wg) { + return new Record(wg, new ProtoRecord(null, null, null, null, null), null); + } + + describe('watch group', () => { + describe("adding records", () => { + it("should add a record", () => { + var wg = new WatchGroup(null, null); + var record = createRecord(wg); + + wg.addRecord(record); + + expect(wg.headRecord).toBe(record); + expect(wg.tailRecord).toBe(record); + expect(wg.headEnabledRecord).toBe(record); + expect(wg.tailEnabledRecord).toBe(record); + }); + + it("should add multiple records", () => { + var wg = new WatchGroup(null, null); + var record1 = createRecord(wg); + var record2 = createRecord(wg); + + wg.addRecord(record1); + wg.addRecord(record2); + + expect(wg.headRecord).toBe(record1); + expect(wg.tailRecord).toBe(record2); + + expect(wg.headEnabledRecord).toBe(record1); + expect(wg.tailEnabledRecord).toBe(record2); + + expect(record1.next).toBe(record2); + expect(record2.prev).toBe(record1); + }); + }); + + describe("enabling/disabling records", () => { + it("should disable a single record", () => { + var wg = new WatchGroup(null, null); + var record = createRecord(wg); + wg.addRecord(record); + + wg.disableRecord(record); + + expect(wg.headEnabledRecord).toBeNull(); + expect(wg.tailEnabledRecord).toBeNull(); + }); + + it("should enable a single record", () => { + var wg = new WatchGroup(null, null); + var record = createRecord(wg); + wg.addRecord(record); + wg.disableRecord(record); + + wg.enableRecord(record); + + expect(wg.headEnabledRecord).toBe(record); + expect(wg.tailEnabledRecord).toBe(record); + }); + + it("should disable a record", () => { + var wg = new WatchGroup(null, null); + var record1 = createRecord(wg); + var record2 = createRecord(wg); + var record3 = createRecord(wg); + var record4 = createRecord(wg); + wg.addRecord(record1); + wg.addRecord(record2); + wg.addRecord(record3); + wg.addRecord(record4); + + wg.disableRecord(record2); + wg.disableRecord(record3); + + expect(record2.disabled).toBeTruthy(); + + expect(wg.headEnabledRecord).toBe(record1); + expect(wg.tailEnabledRecord).toBe(record4); + + expect(record1.nextEnabled).toBe(record4); + expect(record4.prevEnabled).toBe(record1); + }); + + it("should enable a record", () => { + var wg = new WatchGroup(null, null); + var record1 = createRecord(wg); + var record2 = createRecord(wg); + var record3 = createRecord(wg); + var record4 = createRecord(wg); + wg.addRecord(record1); + wg.addRecord(record2); + wg.addRecord(record3); + wg.addRecord(record4); + wg.disableRecord(record2); + wg.disableRecord(record3); + + wg.enableRecord(record2); + wg.enableRecord(record3); + + expect(record1.nextEnabled).toBe(record2); + expect(record2.nextEnabled).toBe(record3); + expect(record3.nextEnabled).toBe(record4); + expect(record4.prevEnabled).toBe(record3); + }); + }) + }); +} \ No newline at end of file