diff --git a/modules/change_detection/src/change_detection.js b/modules/change_detection/src/change_detector.js similarity index 52% rename from modules/change_detection/src/change_detection.js rename to modules/change_detection/src/change_detector.js index 636d17c4b25860..94b737fe9a5260 100644 --- a/modules/change_detection/src/change_detection.js +++ b/modules/change_detection/src/change_detector.js @@ -1,21 +1,23 @@ -import {ProtoWatchGrou, WatchGroup} from './watch_group'; +import {ProtoWatchGroup, WatchGroup} from './watch_group'; import {ProtoRecord, Record} from './record'; import {FIELD} from 'facade/lang'; export * from './record'; export * from './watch_group' -export class ChangeDetection { +export class ChangeDetector { @FIELD('final _rootWatchGroup:WatchGroup') constructor(watchGroup:WatchGroup) { this._rootWatchGroup = watchGroup; } - detectChanges():int { - var current:Record = _rootWatchGroup.headRecord; + detectChanges():number { + var record:Record = this._rootWatchGroup.headRecord; var count:number = 0; - while (current != null) { - if (current.check()) { + for (record = this._rootWatchGroup.headRecord; + record != null; + record = record.checkNext) { + if (record.check()) { count++; } } diff --git a/modules/change_detection/src/facade.dart b/modules/change_detection/src/facade.dart index 41f3bcc83df5e4..25d71bc94594ce 100644 --- a/modules/change_detection/src/facade.dart +++ b/modules/change_detection/src/facade.dart @@ -1,3 +1,14 @@ library change_detection.facade; +@MirrorsUsed(targets: const [FieldGetterFactory], metaTargets: const [] ) +import 'dart:mirrors'; + typedef SetterFn(Object obj, value); + +class FieldGetterFactory { + getter(Object object, String name) { + Symbol symbol = new Symbol(name); + InstanceMirror instanceMirror = reflect(object); + return (Object object) => instanceMirror.getField(symbol).reflectee; + } +} diff --git a/modules/change_detection/src/facade.es6 b/modules/change_detection/src/facade.es6 index 35d97b6d2e80cb..73eb91fdf80bd1 100644 --- a/modules/change_detection/src/facade.es6 +++ b/modules/change_detection/src/facade.es6 @@ -1 +1,7 @@ export var SetterFn = Function; + +export class FieldGetterFactory { + getter(object, name:string) { + return object => object[name]; + } +} diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index 5813604acaac1e..30b6d5877268f1 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -1,48 +1,54 @@ -//import * as wg from './watch_group'; +import {ProtoWatchGroup, WatchGroup} from './watch_group'; import {FIELD} from 'facade/lang'; +import {FieldGetterFactory} from './facade'; /** - * For now we are dropping expression coelescence. We can always add it later, but - * real world numbers should that it does not provide significant benefits. + * For now we are dropping expression coalescence. We can always add it later, but + * real world numbers show that it does not provide significant benefits. */ export class ProtoRecord { - @FIELD('final watchGroup:wg.ProtoWatchGroup') @FIELD('final fieldName:String') /// order list of all records. Including head/tail markers @FIELD('next:ProtoRecord') @FIELD('prev:ProtoRecord') - // Opeque data which will be the target of notification. - // If the object is instance of Record, than it it is directly procssed + // Opaque data which will be the target of notification. + // If the object is instance of Record, than it it is directly processed // Otherwise it is the context used by WatchGroupDispatcher. @FIELD('memento') - @FIELD('_clone') - constructor(watchGroup/*:wg.ProtoWatchGroup*/, fieldName:String, memento) { + constructor(watchGroup:ProtoWatchGroup, fieldName:string, dispatcherCtx) { this.watchGroup = watchGroup; this.fieldName = fieldName; - this.memento = memento; + this.dispatcherCtx = dispatcherCtx; this.next = null; this.prev = null; this.changeNotifier = null; - this._clone = null; - this.changeContext = null; - this.dispatcherContext = null; + this.changeCtx = null; + this.record = null; + } - instantiate(watchGroup/*:wg.WatchGroup*/):Record { - var record = this._clone = new Record(watchGroup, this); - record.prev = this.prev._clone; - record._checkPrev = this.prev._clone; - return _clone; + // todo(vicb) move this logic to the ProtoWatchGroup: + // - meanningless out of the context of ProtoWatchGroup.instantiate(), + // - save a field (record) + instantiate(watchGroup:WatchGroup):Record { + var record = new Record(watchGroup, this); + if (this.prev !== null) { + record.prev = this.prev.record; + record.prev.next = record; + record.checkPrev = this.prev.record; + record.checkPrev.checkNext = record; + // Release the reference to the concrete record once it is not needed any more + this.prev.record = null; + } + if (this.next !== null) { + // Keep a reference to the current concrete record to link the next record + // Not needed for the last one which has no next record + this.record = record; + } + return record; } - instantiateComplete():Record { - var record = this._clone; - record.next = this.next._clone; - record._checkNext = this.next._clone; - this._clone = null; - return this.next; - } } @@ -59,11 +65,8 @@ export class ProtoRecord { * - Atomic watch operations * - Defaults to dirty checking * - Keep this object as lean as possible. (Lean in number of fields) - * - * MEMORY COST: 13 Words; */ export class Record { - @FIELD('final watchGroup:WatchGroup') @FIELD('final protoRecord:ProtoRecord') /// order list of all records. Including head/tail markers @@ -86,57 +89,75 @@ export class Record { this.watchGroup = watchGroup; this.next = null; this.prev = null; - this._checkNext = null; - this._checkPrev = null; - this._notifierNext = null; - - this._mode = MODE_STATE_MARKER; - this._context = null; - this._getter = null; - this._arguments = null; - this.currentValue = null; - this.previousValue = null; + this.checkNext = null; + this.checkPrev = null; + this.notifierNext = null; + + this.mode = MODE_STATE_MARKER; + this.context = null; + this.getter = null; + this.arguments = null; + // The value of this record as observed during the last call to `check()` + this.value = null; } check():bool { - var mode = this._mode; + var mode = this.mode; var state = mode & MODE_MASK_STATE; var notify = mode & MODE_MASK_NOTIFY; - var currentValue; + var newValue; switch (state) { case MODE_STATE_MARKER: return false; case MODE_STATE_PROPERTY: - currentValue = this._getter(this._context); + newValue = this.getter(this.context); break; case MODE_STATE_INVOKE_CLOSURE: - currentValue = this._context(this._arguments); + newValue = this.context(this.arguments); break; case MODE_STATE_INVOKE_METHOD: - currentValue = this._getter(this._context, this._arguments); + newValue = this.getter(this.context, this.arguments); break; case MODE_STATE_MAP: + throw 'not implemented'; case MODE_STATE_LIST: + throw 'not implemented'; + default: + throw 'not implemented'; } - var previousValue = this.previousValue; - if (isSame(previousValue, currentValue)) return false; - if (previousValue instanceof String && currentValue instanceof String - && previousValue == currentValue) { - this.previousValue = currentValue; + + var previousValue = this.value; + if (isSame(previousValue, newValue)) return false; + + this.value = newValue; + + // In Dart, we can have `str1 !== str2` but `str1 == str2` + if (previousValue instanceof String && + newValue instanceof String && + previousValue == newValue) { return false } - this.previousValue = currentValue; - if (this.protoRecord.changeContext instanceof ProtoRecord) { - // forward propaget to the next record + + // todo(vicb): compute this info only once in ctor ? + if (this.protoRecord.dispatcherCtx instanceof ProtoRecord) { + // forward propagate to the next record } else { // notify throught dispatcher - this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatcherContext); + this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatcherCtx); } return true; } + + setContext(context) { + this.mode = MODE_STATE_PROPERTY; + this.context = context; + var factory = new FieldGetterFactory(); + this.getter = factory.getter(context, this.protoRecord.fieldName); + } + } -// The mode is devided into two partes. Which notification mechanism +// The mode is divided into two parts. Which notification mechanism // to use and which dereference mode to execute. // We use dirty checking aka no notification @@ -160,11 +181,7 @@ const MODE_STATE_MAP = 0x0004; const MODE_STATE_LIST = 0x0005; function isSame(a, b) { - if (a === b) { - return true; - } else if ((a !== a) && (b !== b)) { - return true; - } else { - return false; - } + if (a === b) return true; + if ((a !== a) && (b !== b)) return true; + return false; } diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index 1159d5665efa94..cbd0648d4902e1 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -13,38 +13,41 @@ export class ProtoWatchGroup { * Parses [expression] into [ProtoRecord]s and adds them to [ProtoWatchGroup]. * * @param expression The expression to watch - * @param memento an opeque object which will be bassed to WatchGroupDispatcher on + * @param memento an opaque object which will be passed to WatchGroupDispatcher on * detecting a change. * @param shallow Should collections be shallow watched */ watch( - expression:String, + expression:string, memento, - {shallow/*=false*/}:{shallow:bool}) + {shallow/*=false*/} /*:{shallow:boolean}*/) { - /// IMPLEMENT + var protoRecord = new ProtoRecord(this, expression, memento); + + if (this.headRecord === null) { + this.headRecord = this.tailRecord = protoRecord; + } else { + this.tailRecord.next = protoRecord; + protoRecord.prev = this.tailRecord; + this.tailRecord = protoRecord; + } } instantiate(dispatcher:WatchGroupDispatcher):WatchGroup { var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); - var head:Record = null; var tail:Record = null; - var proto:ProtoRecord = this.headRecord; + var proto:ProtoRecord; - while(proto != null) { - tail = proto.instantiate(watchGroup); - if (head == null) head = tail; - proto = proto.next; - } + if (this.headRecord !== null) { + watchGroup.headRecord = tail = this.headRecord.instantiate(watchGroup); - proto = this.headRecord; - while(proto != null) { - proto.instantiateComplete(); - proto = proto.next; + for (proto = this.headRecord.next; proto != null; proto = proto.next) { + tail = proto.instantiate(watchGroup); + } + + watchGroup.tailRecord = tail; } - watchGroup.headRecord = head; - watchGroup.tailRecord = tail; return watchGroup; } @@ -60,14 +63,15 @@ export class WatchGroup { this.dispatcher = dispatcher; this.headRecord = null; this.tailRecord = null; + this.context = null; } insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) { - /// IMPLEMENT + throw 'not implemented'; } remove() { - /// IMPLEMENT + throw 'not implemented'; } /** @@ -75,9 +79,15 @@ export class WatchGroup { * dereference themselves on. Since the WatchGroup can be reused the context * can be re-set many times during the lifetime of the WatchGroup. * - * @param context the new context for change dection for the curren WatchGroup + * @param context the new context for change detection for the current WatchGroup */ setContext(context) { + print('head record ${this.headRecord}'); + for (var record:Record = this.headRecord; + record != null; + record = record.next) { + record.setContext(context); + } } } diff --git a/modules/change_detection/test/change_detection_spec.js b/modules/change_detection/test/change_detection_spec.js deleted file mode 100644 index 07da7050dda89c..00000000000000 --- a/modules/change_detection/test/change_detection_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import {describe, it, xit, expect} from 'test_lib/test_lib'; -import {ProtoWatchGroup, WatchGroup, WatchGroupDispatcher, ChangeDetection} from 'change_detection/change_detection'; - - -export function main() { - describe('change_detection', function() { - describe('ChangeDetection', function() { - xit('should do simple watching', function() { - var person = new Person('misko', 38); - var pwg = new ProtoWatchGroup(); - pwg.watch('name', 'nameToken'); - pwg.watch('age', 'ageToken'); - var dispatcher = new LoggingDispatcher(); - var wg = pwg.instantiate(dispatcher); - wg.setContext(person); - var cd = new ChangeDetection(wg); - cd.detectChanges(); - expect(dispatcher.log).toEqual(['ageToken=38']); - dispatcher.clear(); - cd.detectChanges(); - expect(dispatcher.log).toEqual([]); - person.age=1; - person.name="Misko"; - cd.detectChanges(); - expect(dispatcher.log).toEqual(['nameToken=Misko', 'ageToken=1']); - }); - }); - }); -} - -class Person { - constructor(name:string, age:number) { - this.name = name; - this.age = age; - } -} - -class LoggingDispatcher extends WatchGroupDispatcher { - constructor() { - this.log = null; - } - clear() { - - } -} diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js new file mode 100644 index 00000000000000..aa52f0041009c3 --- /dev/null +++ b/modules/change_detection/test/change_detector_spec.js @@ -0,0 +1,64 @@ +import {describe, it, xit, expect} from 'test_lib/test_lib'; + +import {List, ListWrapper} from 'facade/collection'; + +import { + ChangeDetector, + ProtoWatchGroup, + WatchGroup, + WatchGroupDispatcher +} from 'change_detection/change_detector'; + +import {Record} from 'change_detection/record'; + +export function main() { + describe('change_detection', function() { + describe('ChangeDetection', function() { + it('should do simple watching', function() { + var person = new Person('misko', 38); + var pwg = new ProtoWatchGroup(); + pwg.watch('name', 'name'); + pwg.watch('age', 'age'); + var dispatcher = new LoggingDispatcher(); + var wg = pwg.instantiate(dispatcher); + wg.setContext(person); + var cd = new ChangeDetector(wg); + cd.detectChanges(); + expect(dispatcher.log).toEqual(['name=misko', 'age=38']); + dispatcher.clear(); + cd.detectChanges(); + expect(dispatcher.log).toEqual([]); + person.age = 1; + person.name = "Misko"; + cd.detectChanges(); + expect(dispatcher.log).toEqual(['name=Misko', 'age=1']); + }); + }); + }); +} + +class Person { + constructor(name:string, age:number) { + this.name = name; + this.age = age; + } + + toString() { + return 'name=' + this.name + ' age=' + this.age.toString(); + } +} + +class LoggingDispatcher extends WatchGroupDispatcher { + constructor() { + this.log = null; + this.clear(); + } + + clear() { + this.log = ListWrapper.create(); + } + + onRecordChange(record:Record, context) { + ListWrapper.push(this.log, context + '=' + record.value.toString()); + } +}