design: add changed detection API

This commit is contained in:
Misko Hevery 2014-09-26 11:20:08 -07:00
parent 7e3005e705
commit 6335fc407c
10 changed files with 195 additions and 71 deletions

View File

@ -1,5 +1,22 @@
import {WatchGroup} from './watch_group';
import {Record} from './record';
export class ChangeDetection { export class ChangeDetection {
detectChanges():int {} @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

@ -1,3 +0,0 @@
export class ProtoRecord {
}

View File

@ -1,27 +0,0 @@
export class ProtoWatchGroup {
watch(
expression:String,
context:dynamic,
{isCollection})
{
}
}
/*
@Component(
bind: {
'title': 'title',
'name': 'name'
}
)
class MyComponent implements ChangeListener {
String name;
String title;
onChange(List<Record> changes) {
}
}
*/

View File

@ -1,4 +1,54 @@
import {WatchGroup} from './watch_group'; import {ProtoWatchGroup, WatchGroup} from './watch_group';
export class ProtoRecord {
@FIELD('final watchGroup:ProtoWatchGroup')
@FIELD('final fieldName:String')
/// order list of all records. Including head/tail markers
@FIELD('next:ProtoRecord')
@FIELD('prev:ProtoRecord')
/// next record to dirty check
@FIELD('_checkNext:ProtoRecord')
@FIELD('_checkPrev:ProtoRecord')
// next notifier
@FIELD('_notifierNext:ProtoRecord')
// Opeque data which will be presented to WatchGroupDispatcher
@FIELD('dispatcherContext')
// IF we detect change, we have to update the _context of the
// next record.
@FIELD('_updateContext:ProtoRecord')
// May be removed if we don't support coelsence.
@FIELD('_updateContextNext:ProtoRecord')
@FIELD('_clone')
constructor(watchGroup:ProtoWatchGroup, fieldName:String) {
this.watchGroup = watchGroup;
this.fieldName = fieldName;
this._next = null;
this._prev = null;
this._checkNext = null;
this._checkPrev = null;
this._notifierNext = null;
this.dispatcherContext = null;
this._updateContext = null;
this._updateContextNext = null;
this._clone = null;
}
instantiate(watchGroup:WatchGroup):Record {
var record = this._clone = new Record(watchGroup, this);
record._prev = this._prev._clone;
record._checkPrev = this._checkPrev._clone;
return _clone;
}
instantiateComplete():Record {
var record = this._clone;
record._next = this._next._clone;
record._checkNext = this._checkNext._clone;
this._clone = null;
return this._next;
}
}
/** /**
@ -19,13 +69,8 @@ import {WatchGroup} from './watch_group';
*/ */
export class Record { export class Record {
@FIELD('final _watchGroup:WatchGroup') @FIELD('final watchGroup:WatchGroup')
@FIELD('final _protoRecord:ProtoRecord') @FIELD('final protoRecord:ProtoRecord')
@FIELD('_context')
@FIELD('_getter')
@FIELD('_arguments')
@FIELD('_previousValue')
@FIELD('_mode:int')
/// order list of all records. Including head/tail markers /// order list of all records. Including head/tail markers
@FIELD('_next:Record') @FIELD('_next:Record')
@FIELD('_prev:Record') @FIELD('_prev:Record')
@ -37,18 +82,40 @@ export class Record {
// notifier context will be present to the notifier to release // notifier context will be present to the notifier to release
// the object from notification/watching. // the object from notification/watching.
@FIELD('_notifierContext') @FIELD('_notifierContext')
// Opeque data which will be presented to WatchGroupDispatcher
@FIELD('_watchContext')
// IF we detect change, we have to update the _context of the // IF we detect change, we have to update the _context of the
// next record. // next record.
@FIELD('_updateContext:Record') @FIELD('_updateContext:Record')
// May be removed if we don't support coelsence. // May be removed if we don't support coelsence.
@FIELD('_updateContextNext:Record') @FIELD('_updateContextNext:Record')
constructor() {
@FIELD('_mode:int')
@FIELD('_context')
@FIELD('_getter')
@FIELD('_arguments')
@FIELD('currentValue')
@FIELD('previousValue')
constructor(watchGroup:WatchGroup, protoRecord:ProtoRecord) {
this.protoRecord = protoRecord;
this.watchGroup = watchGroup;
this._next = null;
this._prev = null;
this._checkNext = null;
this._checkPrev = null;
this._notifierNext = null;
this._notifierContext = null;
this._updateContext = null;
this._updateContextNext = null;
this._mode = MODE_STATE_MARKER;
this._context = null;
this._getter = null;
this._arguments = null;
this.currentValue = null;
this.previousValue = null;
} }
check():bool { check():bool {
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 currentValue;
@ -67,14 +134,15 @@ export class Record {
case MODE_STATE_MAP: case MODE_STATE_MAP:
case MODE_STATE_LIST: case MODE_STATE_LIST:
} }
var previousValue = this._previousValue; var previousValue = this.previousValue;
if (isSame(previousValue, currentValue)) return false; if (isSame(previousValue, currentValue)) return false;
if (previousValue instanceof String && currentValue instanceof String if (previousValue instanceof String && currentValue instanceof String
&& previousValue == currentValue) { && previousValue == currentValue) {
this._previousValue = currentValue; this.previousValue = currentValue;
return false return false
} }
this.previousValue = previousValue; this.previousValue = currentValue;
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatcherContext);
return true; return true;
} }
} }
@ -83,24 +151,24 @@ export class Record {
// 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
var MODE_MASK_NOTIFY:number = 0xFF00; const MODE_MASK_NOTIFY = 0xFF00;
// Encodes the state of dereference // Encodes the state of dereference
var MODE_MASK_STATE:int = 0x00FF; const MODE_MASK_STATE = 0x00FF;
var MODE_PLUGIN_DIRTY_CHECK:int = 0x0000; const MODE_PLUGIN_DIRTY_CHECK = 0x0000;
var MODE_STATE_MARKER:int = 0x0000; const MODE_STATE_MARKER = 0x0000;
/// _context[_protoRecord.propname] => _getter(_context) /// _context[_protoRecord.propname] => _getter(_context)
var MODE_STATE_PROPERTY:int = 0x0001; const MODE_STATE_PROPERTY = 0x0001;
/// _context(_arguments) /// _context(_arguments)
var MODE_STATE_INVOKE_CLOSURE:int = 0x0002; const MODE_STATE_INVOKE_CLOSURE = 0x0002;
/// _getter(_context, _arguments) /// _getter(_context, _arguments)
var MODE_STATE_INVOKE_METHOD:int = 0x0003; const MODE_STATE_INVOKE_METHOD = 0x0003;
/// _context is Map => _previousValue is MapChangeRecord /// _context is Map => _previousValue is MapChangeRecord
var MODE_STATE_MAP:int = 0x0004; const MODE_STATE_MAP = 0x0004;
/// _context is Array/List/Iterable => _previousValue = ListChangeRecord /// _context is Array/List/Iterable => _previousValue = ListChangeRecord
var MODE_STATE_LIST:int = 0x0005; const MODE_STATE_LIST = 0x0005;
function isSame(a, b) { function isSame(a, b) {
if (a === b) { if (a === b) {

View File

@ -1,4 +1,63 @@
export class WatchGroup { import {ProtoRecord, Record} from './record';
@FIELD('final dispatcher:WatchGroupDispatcher') import {WatchGroupDispatcher} from './watch_group_dispatcher';
constructor() {}
export class ProtoWatchGroup {
@FIELD('final _headRecord:ProtoRecord')
@FIELD('final _tailRecord:ProtoRecord')
constructor() {
this._headRecord = null;
this._tailRecord = null;
}
watch(
expression:String,
context,
{isCollection})
{
/// IMPREMENT
}
instantiate(dispatcher:WatchGroupDispatcher):WatchGroup {
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
var head:Record = null;
var tail:Record = null;
var proto:ProtoRecord = this._headRecord;
while(proto != null) {
tail = proto.instantiate(watchGroup);
if (head == null) head = tail;
proto = proto.next;
}
proto = this._headRecord;
while(proto != null) {
proto = proto.instantiateComplete();
}
watchGroup._headRecord = head;
watchGroup._tailRecord = tail;
return watchGroup;
}
}
export class WatchGroup {
@FIELD('final protoWatchGroup:ProtoWatchGroup')
@FIELD('final dispatcher:WatchGroupDispatcher')
@FIELD('final _headRecord:Record')
@FIELD('final _tailRecord:Record')
constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) {
this.protoWatchGroup = protoWatchGroup;
this.dispatcher = dispatcher;
this._headRecord = null;
this._tailRecord = null;
}
insertChildGroup(newChild:WatchGroup, insertAfter:WatchGroup) {
/// IMPLEMENT
}
remove() {
/// IMPLEMENT
}
} }

View File

@ -1,4 +1,5 @@
import {Record} from './record';
export class WatchGroupDispatcher { export class WatchGroupDispatcher {
notify(record:Record, context) {} onRecordChange(record:Record, context) {}
} }

View File

@ -6,6 +6,7 @@ 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_detection';
export * from 'change_detection/watch_group';
export * from 'change_detection/record'; export * from 'change_detection/record';
export * from './compiler/compiler'; export * from './compiler/compiler';

View File

@ -1,5 +1,6 @@
import {Node, DocumentFragment} from 'facade/dom'; import {Node, DocumentFragment} from 'facade/dom';
import {ListWrapper wraps List} from 'facade/collection'; import {ListWrapper wraps List} from 'facade/collection';
import {WatchGroupDispatcher} from 'change_detection/watch_group_dispatcher';
import {Record} from 'change_detection/record'; import {Record} from 'change_detection/record';
@IMPLEMENTS(WatchGroupDispatcher) @IMPLEMENTS(WatchGroupDispatcher)
@ -18,17 +19,17 @@ export class View {
this._nodes = ListWrapper.clone(fragment.childNodes); this._nodes = ListWrapper.clone(fragment.childNodes);
} }
notify(record:Record, target) { onRecordChange(record:Record, target) {
/*
// dispatch to element injector or text nodes based on context // dispatch to element injector or text nodes based on context
if (Number.is(target)) { if (target is ElementInjectorTarge) {
// we know it refferst to _textNodes.
} else {
// we know that it is ElementInjectorTarge // we know that it is ElementInjectorTarge
var eTarget:ElementInjectorTarget = target; var eTarget:ElementInjectorTarget = target;
onChangeDispatcher.notify(this, eTarget); onChangeDispatcher.notify(this, eTarget);
eTarget.invoke(record, _elementInjectors); eTarget.invoke(record, _elementInjectors);
} else {
// we know it refferst to _textNodes.
var textNodeIndex:number = target;
DOM.setText(this._textNodes[textNodeIndex], record.currentValue);
} }
*/
} }
} }

View File

@ -2,7 +2,7 @@ library angular.core.facade.dom;
import 'dart:html'; import 'dart:html';
export 'dart:html' show DocumentFragment, Node, Element, TemplateElement; export 'dart:html' show DocumentFragment, Node, Element, TemplateElement, Text;
class DOM { class DOM {
static query(selector) { static query(selector) {
@ -14,7 +14,10 @@ class DOM {
static getInnerHTML(el) { static getInnerHTML(el) {
return el.innerHtml; return el.innerHtml;
} }
static setInnerHTML(el, value) { static setInnerHTML(el:, value) {
el.innerHtml = value; el.innerHtml = value;
} }
static setText(Text text, String value) {
text.text = value;
}
} }

View File

@ -1,5 +1,6 @@
export var DocumentFragment = window.DocumentFragment; export var DocumentFragment = window.DocumentFragment;
export var Node = window.Node; export var Node = window.Node;
export var Text = window.Text;
export var Element = window.HTMLElement; export var Element = window.HTMLElement;
export var TemplateElement = window.HTMLTemplateElement; export var TemplateElement = window.HTMLTemplateElement;
@ -16,4 +17,7 @@ export class DOM {
static setInnerHTML(el, value) { static setInnerHTML(el, value) {
el.innerHTML = value; el.innerHTML = value;
} }
static setText(text:Text, value:String) {
text.nodeValue = value;
}
} }