feature(change detection): implement barebone ChangeDetector

fixes #39
This commit is contained in:
Victor Berchet 2014-10-02 15:14:32 +02:00
parent 38340ce8d9
commit 5527a1b1a4
9 changed files with 214 additions and 159 deletions

View File

@ -1,24 +0,0 @@
import {ProtoWatchGrou, WatchGroup} from './watch_group';
import {ProtoRecord, Record} from './record';
import {FIELD} from 'facade/lang';
export * from './record';
export * from './watch_group'
export class ChangeDetection {
@FIELD('final _rootWatchGroup:WatchGroup')
constructor(watchGroup:WatchGroup) {
this._rootWatchGroup = watchGroup;
}
detectChanges():int {
var current:Record = _rootWatchGroup.headRecord;
var count:number = 0;
while (current != null) {
if (current.check()) {
count++;
}
}
return count;
}
}

View File

@ -0,0 +1,26 @@
import {ProtoWatchGroup, WatchGroup} from './watch_group';
import {ProtoRecord, Record} from './record';
import {FIELD, int} from 'facade/lang';
export * from './record';
export * from './watch_group'
export class ChangeDetector {
@FIELD('final _rootWatchGroup:WatchGroup')
constructor(watchGroup:WatchGroup) {
this._rootWatchGroup = watchGroup;
}
detectChanges():int {
var record:Record = this._rootWatchGroup.headRecord;
var count:int = 0;
for (record = this._rootWatchGroup.headRecord;
record != null;
record = record.checkNext) {
if (record.check()) {
count++;
}
}
return count;
}
}

View File

@ -1,3 +1,14 @@
library change_detection.facade; library change_detection.facade;
@MirrorsUsed(targets: const [FieldGetterFactory], metaTargets: const [] )
import 'dart:mirrors';
typedef SetterFn(Object obj, value); 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;
}
}

View File

@ -1 +1,7 @@
export var SetterFn = Function; export var SetterFn = Function;
export class FieldGetterFactory {
getter(object, name:string) {
return new Function('o', 'return o["' + name + '"]');
}
}

View File

@ -1,48 +1,28 @@
//import * as wg from './watch_group'; import {ProtoWatchGroup, WatchGroup} from './watch_group';
import {FIELD} from 'facade/lang'; import {FIELD} from 'facade/lang';
import {FieldGetterFactory} from './facade';
/** /**
* For now we are dropping expression coelescence. We can always add it later, but * For now we are dropping expression coalescence. We can always add it later, but
* real world numbers should that it does not provide significant benefits. * real world numbers show that it does not provide significant benefits.
*/ */
export class ProtoRecord { export class ProtoRecord {
@FIELD('final watchGroup:wg.ProtoWatchGroup') @FIELD('final watchGroup:wg.ProtoWatchGroup')
@FIELD('final fieldName:String') @FIELD('final fieldName:String')
/// order list of all records. Including head/tail markers /// order list of all records. Including head/tail markers
@FIELD('next:ProtoRecord') @FIELD('next:ProtoRecord')
@FIELD('prev:ProtoRecord') @FIELD('prev:ProtoRecord')
// Opeque data which will be the target of notification. // Opaque data which will be the target of notification.
// If the object is instance of Record, than it it is directly procssed // If the object is instance of Record, than it it is directly processed
// Otherwise it is the context used by WatchGroupDispatcher. // Otherwise it is the context used by WatchGroupDispatcher.
@FIELD('memento') @FIELD('memento')
@FIELD('_clone') constructor(watchGroup:ProtoWatchGroup, fieldName:string, dispatchMemento) {
constructor(watchGroup/*:wg.ProtoWatchGroup*/, fieldName:String, memento) {
this.watchGroup = watchGroup; this.watchGroup = watchGroup;
this.fieldName = fieldName; this.fieldName = fieldName;
this.memento = memento; this.dispatchMemento = dispatchMemento;
this.next = null; this.next = null;
this.prev = null; this.prev = null;
this.changeNotifier = null;
this._clone = null;
this.changeContext = null;
this.dispatcherContext = 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;
}
instantiateComplete():Record {
var record = this._clone;
record.next = this.next._clone;
record._checkNext = this.next._clone;
this._clone = null;
return this.next;
}
} }
@ -59,11 +39,8 @@ export class ProtoRecord {
* - Atomic watch operations * - Atomic watch operations
* - Defaults to dirty checking * - Defaults to dirty checking
* - Keep this object as lean as possible. (Lean in number of fields) * - Keep this object as lean as possible. (Lean in number of fields)
*
* MEMORY COST: 13 Words;
*/ */
export class Record { export class Record {
@FIELD('final watchGroup:WatchGroup') @FIELD('final watchGroup:WatchGroup')
@FIELD('final protoRecord:ProtoRecord') @FIELD('final protoRecord:ProtoRecord')
/// order list of all records. Including head/tail markers /// order list of all records. Including head/tail markers
@ -79,64 +56,93 @@ export class Record {
@FIELD('_context') @FIELD('_context')
@FIELD('_getter') @FIELD('_getter')
@FIELD('_arguments') @FIELD('_arguments')
@FIELD('currentValue')
@FIELD('previousValue') @FIELD('previousValue')
constructor(watchGroup/*:wg.WatchGroup*/, protoRecord:ProtoRecord) { constructor(watchGroup/*:wg.WatchGroup*/, protoRecord:ProtoRecord) {
this.protoRecord = protoRecord; this.protoRecord = protoRecord;
this.watchGroup = watchGroup; this.watchGroup = watchGroup;
this.next = null; this.next = null;
this.prev = null; this.prev = null;
this._checkNext = null; this.checkNext = null;
this._checkPrev = null; this.checkPrev = null;
this._notifierNext = null; this.notifierNext = null;
this._mode = MODE_STATE_MARKER; this.mode = MODE_STATE_MARKER;
this._context = null; this.context = null;
this._getter = null; this.getter = null;
this._arguments = null; this.arguments = null;
this.currentValue = null;
this.previousValue = null; this.previousValue = null;
this.currentValue = null;
} }
check():bool { check():boolean {
var mode = this._mode; var mode = this.mode;
var state = mode & MODE_MASK_STATE; var state = mode & MODE_MASK_STATE;
var notify = mode & MODE_MASK_NOTIFY; var notify = mode & MODE_MASK_NOTIFY;
var currentValue; var newValue;
switch (state) { switch (state) {
case MODE_STATE_MARKER: case MODE_STATE_MARKER:
return false; return false;
case MODE_STATE_PROPERTY: case MODE_STATE_PROPERTY:
currentValue = this._getter(this._context); newValue = this.getter(this.context);
break; break;
case MODE_STATE_INVOKE_CLOSURE: case MODE_STATE_INVOKE_CLOSURE:
currentValue = this._context(this._arguments); newValue = this.context(this.arguments);
break; break;
case MODE_STATE_INVOKE_METHOD: case MODE_STATE_INVOKE_METHOD:
currentValue = this._getter(this._context, this._arguments); newValue = this.getter(this.context, this.arguments);
break; break;
case MODE_STATE_MAP: case MODE_STATE_MAP:
throw 'not implemented';
case MODE_STATE_LIST: 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 var previousValue = this.currentValue;
&& previousValue == currentValue) { if (previousValue === this) {
this.previousValue = currentValue; // When the record is checked for the first time we should always notify
return false this.currentValue = newValue;
} this.previousValue = previousValue = null;
this.previousValue = currentValue;
if (this.protoRecord.changeContext instanceof ProtoRecord) {
// forward propaget to the next record
} else { } else {
// notify throught dispatcher this.currentValue = newValue;
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatcherContext); this.previousValue = previousValue;
if (isSame(previousValue, newValue)) return false;
// In Dart, we can have `str1 !== str2` but `str1 == str2`
if (previousValue instanceof String &&
newValue instanceof String &&
previousValue == newValue) {
return false
}
} }
// todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
if (this.protoRecord.dispatchMemento === null) {
// forward propagate to the next record
} else {
// notify through dispatcher
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatchMemento);
}
return true; return true;
} }
setContext(context) {
// use `this` as a marker for a fresh record
this.currentValue = this;
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. // to use and which dereference mode to execute.
// We use dirty checking aka no notification // We use dirty checking aka no notification
@ -160,11 +166,7 @@ const MODE_STATE_MAP = 0x0004;
const MODE_STATE_LIST = 0x0005; const MODE_STATE_LIST = 0x0005;
function isSame(a, b) { function isSame(a, b) {
if (a === b) { if (a === b) return true;
return true; if ((a !== a) && (b !== b)) return true;
} else if ((a !== a) && (b !== b)) { return false;
return true;
} else {
return false;
}
} }

View File

@ -13,38 +13,46 @@ export class ProtoWatchGroup {
* Parses [expression] into [ProtoRecord]s and adds them to [ProtoWatchGroup]. * Parses [expression] into [ProtoRecord]s and adds them to [ProtoWatchGroup].
* *
* @param expression The expression to watch * @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. * detecting a change.
* @param shallow Should collections be shallow watched * @param shallow Should collections be shallow watched
*/ */
watch( watch(expression:string,
expression:String, memento,
memento, shallow /*= false*/) // TODO(vicb): comment out when opt-params are supported
{shallow/*=false*/}:{shallow:bool})
{ {
/// 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 { instantiate(dispatcher:WatchGroupDispatcher):WatchGroup {
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
var head:Record = null;
var tail:Record = null; var tail:Record = null;
var proto:ProtoRecord = this.headRecord; var proto:ProtoRecord;
var prevRecord:Record = null;
while(proto != null) { if (this.headRecord !== null) {
tail = proto.instantiate(watchGroup); watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord);
if (head == null) head = tail;
proto = proto.next; for (proto = this.headRecord.next; proto != null; proto = proto.next) {
prevRecord = tail;
tail = new Record(watchGroup, proto);
tail.prev = prevRecord;
prevRecord.next = tail;
tail.checkPrev = prevRecord;
prevRecord.checkNext = tail;
}
watchGroup.tailRecord = tail;
} }
proto = this.headRecord;
while(proto != null) {
proto.instantiateComplete();
proto = proto.next;
}
watchGroup.headRecord = head;
watchGroup.tailRecord = tail;
return watchGroup; return watchGroup;
} }
@ -60,14 +68,15 @@ export class WatchGroup {
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.headRecord = null; this.headRecord = null;
this.tailRecord = null; this.tailRecord = null;
this.context = null;
} }
insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) { insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) {
/// IMPLEMENT throw 'not implemented';
} }
remove() { remove() {
/// IMPLEMENT throw 'not implemented';
} }
/** /**
@ -75,12 +84,18 @@ export class WatchGroup {
* dereference themselves on. Since the WatchGroup can be reused the context * dereference themselves on. Since the WatchGroup can be reused the context
* can be re-set many times during the lifetime of the WatchGroup. * 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) { setContext(context) {
for (var record:Record = this.headRecord;
record != null;
record = record.next) {
record.setContext(context);
}
} }
} }
export class WatchGroupDispatcher { export class WatchGroupDispatcher {
// The record holds the previous value at the time of the call
onRecordChange(record:Record, context) {} onRecordChange(record:Record, context) {}
} }

View File

@ -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() {
}
}

View File

@ -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', false); // TODO(vicb): remove opt shallow when supported
pwg.watch('age', 'age', false);
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.currentValue.toString());
}
}

View File

@ -5,7 +5,7 @@ export * from './annotations/directive';
export * from './annotations/component'; export * from './annotations/component';
export * from './annotations/template_config'; export * from './annotations/template_config';
export * from 'change_detection/change_detection'; export * from 'change_detection/change_detector';
export * from 'change_detection/watch_group'; export * from 'change_detection/watch_group';
export * from 'change_detection/record'; export * from 'change_detection/record';