feat(change_detection): add support for binary operations and literals
This commit is contained in:
parent
b8e3617a1d
commit
79a9430f2c
|
@ -16,7 +16,7 @@ export class ChangeDetector {
|
||||||
var count:int = 0;
|
var count:int = 0;
|
||||||
for (record = this._rootWatchGroup.headRecord;
|
for (record = this._rootWatchGroup.headRecord;
|
||||||
record != null;
|
record != null;
|
||||||
record = record.checkNext) {
|
record = record.next) {
|
||||||
if (record.check()) {
|
if (record.check()) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class AST {
|
||||||
throw new BaseException("Not supported");
|
throw new BaseException("Not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ export class ImplicitReceiver extends AST {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitImplicitReceiver(this);
|
visitor.visitImplicitReceiver(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ export class Chain extends AST {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitChain(this);
|
visitor.visitChain(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +70,8 @@ export class Conditional extends AST {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitConditional(this);
|
visitor.visitConditional(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +99,8 @@ export class AccessMember extends AST {
|
||||||
return this.setter(this.receiver.eval(context), value);
|
return this.setter(this.receiver.eval(context), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitAccessMember(this);
|
visitor.visitAccessMember(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +143,8 @@ export class KeyedAccess extends AST {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitKeyedAccess(this);
|
visitor.visitKeyedAccess(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,8 +159,8 @@ export class Formatter extends AST {
|
||||||
this.allArgs = ListWrapper.concat([exp], args);
|
this.allArgs = ListWrapper.concat([exp], args);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitFormatter(this);
|
visitor.visitFormatter(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,8 +174,8 @@ export class LiteralPrimitive extends AST {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitLiteralPrimitive(this);
|
visitor.visitLiteralPrimitive(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +189,8 @@ export class LiteralArray extends AST {
|
||||||
return ListWrapper.map(this.expressions, (e) => e.eval(context));
|
return ListWrapper.map(this.expressions, (e) => e.eval(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitLiteralArray(this);
|
visitor.visitLiteralArray(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,8 +210,8 @@ export class LiteralMap extends AST {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitLiteralMap(this);
|
visitor.visitLiteralMap(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,8 +256,8 @@ export class Binary extends AST {
|
||||||
throw 'Internal error [$operation] not handled';
|
throw 'Internal error [$operation] not handled';
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitBinary(this);
|
visitor.visitBinary(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,8 +272,8 @@ export class PrefixNot extends AST {
|
||||||
return !this.expression.eval(context);
|
return !this.expression.eval(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitPrefixNot(this);
|
visitor.visitPrefixNot(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +289,8 @@ export class Assignment extends AST {
|
||||||
return this.target.assign(context, this.value.eval(context));
|
return this.target.assign(context, this.value.eval(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitAssignment(this);
|
visitor.visitAssignment(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,8 +309,8 @@ export class MethodCall extends AST {
|
||||||
return this.fn(obj, evalList(context, this.args));
|
return this.fn(obj, evalList(context, this.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitMethodCall(this);
|
visitor.visitMethodCall(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,27 +332,27 @@ export class FunctionCall extends AST {
|
||||||
return FunctionWrapper.apply(obj, evalList(context, this.args));
|
return FunctionWrapper.apply(obj, evalList(context, this.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(visitor) {
|
visit(visitor, args) {
|
||||||
visitor.visitFunctionCall(this);
|
visitor.visitFunctionCall(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERFACE
|
//INTERFACE
|
||||||
export class AstVisitor {
|
export class AstVisitor {
|
||||||
visitChain(ast:Chain){}
|
visitChain(ast:Chain, args){}
|
||||||
visitImplicitReceiver(ast:ImplicitReceiver) {}
|
visitImplicitReceiver(ast:ImplicitReceiver, args) {}
|
||||||
visitConditional(ast:Conditional) {}
|
visitConditional(ast:Conditional, args) {}
|
||||||
visitAccessMember(ast:AccessMember) {}
|
visitAccessMember(ast:AccessMember, args) {}
|
||||||
visitKeyedAccess(ast:KeyedAccess) {}
|
visitKeyedAccess(ast:KeyedAccess, args) {}
|
||||||
visitBinary(ast:Binary) {}
|
visitBinary(ast:Binary, args) {}
|
||||||
visitPrefixNot(ast:PrefixNot) {}
|
visitPrefixNot(ast:PrefixNot, args) {}
|
||||||
visitLiteralPrimitive(ast:LiteralPrimitive) {}
|
visitLiteralPrimitive(ast:LiteralPrimitive, args) {}
|
||||||
visitFormatter(ast:Formatter) {}
|
visitFormatter(ast:Formatter, args) {}
|
||||||
visitAssignment(ast:Assignment) {}
|
visitAssignment(ast:Assignment, args) {}
|
||||||
visitLiteralArray(ast:LiteralArray) {}
|
visitLiteralArray(ast:LiteralArray, args) {}
|
||||||
visitLiteralMap(ast:LiteralMap) {}
|
visitLiteralMap(ast:LiteralMap, args) {}
|
||||||
visitMethodCall(ast:MethodCall) {}
|
visitMethodCall(ast:MethodCall, args) {}
|
||||||
visitFunctionCall(ast:FunctionCall) {}
|
visitFunctionCall(ast:FunctionCall, args) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
|
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
|
||||||
|
|
|
@ -1,27 +1,43 @@
|
||||||
import {ProtoWatchGroup, WatchGroup} from './watch_group';
|
import {ProtoWatchGroup, WatchGroup} from './watch_group';
|
||||||
import {FIELD} from 'facade/lang';
|
import {FIELD, isPresent, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
|
||||||
|
import {ListWrapper} from 'facade/collection';
|
||||||
import {ClosureMap} from 'change_detection/parser/closure_map';
|
import {ClosureMap} from 'change_detection/parser/closure_map';
|
||||||
|
|
||||||
|
var _fresh = new Object();
|
||||||
|
|
||||||
|
export const PROTO_RECORD_CONST = 'const';
|
||||||
|
export const PROTO_RECORD_FUNC = 'func';
|
||||||
|
export const PROTO_RECORD_PROPERTY = 'property';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For now we are dropping expression coalescence. We can always add it later, but
|
* 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.
|
* 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 context:Object')
|
||||||
/// order list of all records. Including head/tail markers
|
@FIELD('final arity:int')
|
||||||
|
@FIELD('final dest')
|
||||||
|
|
||||||
@FIELD('next:ProtoRecord')
|
@FIELD('next:ProtoRecord')
|
||||||
@FIELD('prev:ProtoRecord')
|
@FIELD('prev:ProtoRecord')
|
||||||
// Opaque data which will be the target of notification.
|
@FIELD('recordInConstruction:Record')
|
||||||
// If the object is instance of Record, than it it is directly processed
|
constructor(watchGroup:ProtoWatchGroup,
|
||||||
// Otherwise it is the context used by WatchGroupDispatcher.
|
recordType:string,
|
||||||
@FIELD('memento')
|
funcOrValue,
|
||||||
constructor(watchGroup:ProtoWatchGroup, fieldName:string, dispatchMemento) {
|
arity:int,
|
||||||
|
dest) {
|
||||||
|
|
||||||
this.watchGroup = watchGroup;
|
this.watchGroup = watchGroup;
|
||||||
this.fieldName = fieldName;
|
this.recordType = recordType;
|
||||||
this.dispatchMemento = dispatchMemento;
|
this.funcOrValue = funcOrValue;
|
||||||
|
this.arity = arity;
|
||||||
|
this.dest = dest;
|
||||||
|
|
||||||
this.next = null;
|
this.next = null;
|
||||||
this.prev = null;
|
this.prev = null;
|
||||||
|
|
||||||
|
this.recordInConstruction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,101 +59,114 @@ export class ProtoRecord {
|
||||||
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
|
|
||||||
@FIELD('next:Record')
|
@FIELD('next:Record')
|
||||||
@FIELD('prev:Record')
|
@FIELD('prev:Record')
|
||||||
/// next record to dirty check
|
@FIELD('dest:Record')
|
||||||
@FIELD('checkNext:Record')
|
|
||||||
@FIELD('checkPrev:Record')
|
@FIELD('previousValue')
|
||||||
// next notifier
|
@FIELD('currentValue')
|
||||||
@FIELD('notifierNext:Record')
|
|
||||||
|
|
||||||
@FIELD('mode:int')
|
@FIELD('mode:int')
|
||||||
@FIELD('context')
|
@FIELD('context')
|
||||||
@FIELD('getter')
|
@FIELD('funcOrValue')
|
||||||
@FIELD('previousValue')
|
@FIELD('args:List')
|
||||||
@FIELD('currentValue')
|
|
||||||
constructor(watchGroup/*:wg.WatchGroup*/, protoRecord:ProtoRecord) {
|
// Opaque data which will be the target of notification.
|
||||||
this.protoRecord = protoRecord;
|
// If the object is instance of Record, then it it is directly processed
|
||||||
|
// Otherwise it is the context used by WatchGroupDispatcher.
|
||||||
|
@FIELD('dest')
|
||||||
|
|
||||||
|
constructor(watchGroup:WatchGroup, protoRecord:ProtoRecord) {
|
||||||
this.watchGroup = watchGroup;
|
this.watchGroup = watchGroup;
|
||||||
|
this.protoRecord = protoRecord;
|
||||||
|
|
||||||
this.next = null;
|
this.next = null;
|
||||||
this.prev = null;
|
this.prev = null;
|
||||||
this.checkNext = null;
|
this.dest = null;
|
||||||
this.checkPrev = null;
|
|
||||||
this.notifierNext = null;
|
|
||||||
|
|
||||||
this.mode = MODE_STATE_MARKER;
|
|
||||||
this.context = null;
|
|
||||||
this.getter = null;
|
|
||||||
this.arguments = null;
|
|
||||||
this.previousValue = null;
|
this.previousValue = null;
|
||||||
// `this` means that the record is fresh
|
this.currentValue = _fresh;
|
||||||
this.currentValue = this;
|
|
||||||
|
this.mode = null;
|
||||||
|
this.context = null;
|
||||||
|
this.funcOrValue = null;
|
||||||
|
this.args = null;
|
||||||
|
|
||||||
|
if (protoRecord.recordType === PROTO_RECORD_CONST) {
|
||||||
|
this.mode = MODE_STATE_CONST;
|
||||||
|
this.funcOrValue = protoRecord.funcOrValue;
|
||||||
|
|
||||||
|
} else if (protoRecord.recordType === PROTO_RECORD_FUNC) {
|
||||||
|
this.mode = MODE_STATE_INVOKE_FUNCTION;
|
||||||
|
this.funcOrValue = protoRecord.funcOrValue;
|
||||||
|
this.args = ListWrapper.createFixedSize(protoRecord.arity);
|
||||||
|
|
||||||
|
} else if (protoRecord.recordType === PROTO_RECORD_PROPERTY) {
|
||||||
|
this.mode = MODE_STATE_PROPERTY;
|
||||||
|
this.funcOrValue = protoRecord.funcOrValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
check():boolean {
|
check():boolean {
|
||||||
var mode = this.mode;
|
this.previousValue = this.currentValue;
|
||||||
var state = mode & MODE_MASK_STATE;
|
this.currentValue = this._calculateNewValue();
|
||||||
var notify = mode & MODE_MASK_NOTIFY;
|
|
||||||
var newValue;
|
|
||||||
switch (state) {
|
|
||||||
case MODE_STATE_MARKER:
|
|
||||||
return false;
|
|
||||||
case MODE_STATE_PROPERTY:
|
|
||||||
newValue = this.getter(this.context);
|
|
||||||
break;
|
|
||||||
case MODE_STATE_INVOKE_CLOSURE:
|
|
||||||
newValue = this.context(this.arguments);
|
|
||||||
break;
|
|
||||||
case MODE_STATE_INVOKE_METHOD:
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isSame(this.previousValue, this.currentValue)) return false;
|
||||||
|
|
||||||
var previousValue = this.currentValue;
|
this._updateDestination();
|
||||||
if (previousValue === this) {
|
|
||||||
// When the record is checked for the first time we should always notify
|
|
||||||
this.currentValue = newValue;
|
|
||||||
this.previousValue = previousValue = null;
|
|
||||||
} else {
|
|
||||||
this.currentValue = newValue;
|
|
||||||
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) {
|
|
||||||
this.next.setContext(this.currentValue);
|
|
||||||
} else {
|
|
||||||
// notify through dispatcher
|
|
||||||
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatchMemento);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(context) {
|
_updateDestination() {
|
||||||
this.mode = MODE_STATE_PROPERTY;
|
// todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
|
||||||
this.context = context;
|
if (this.dest instanceof Record) {
|
||||||
var closureMap = new ClosureMap();
|
if (isPresent(this.protoRecord.dest.position)) {
|
||||||
this.getter = closureMap.getter(this.protoRecord.fieldName);
|
this.dest.updateArg(this.currentValue, this.protoRecord.dest.position);
|
||||||
|
} else {
|
||||||
|
this.dest.updateContext(this.currentValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_calculateNewValue() {
|
||||||
|
var state = this.mode;
|
||||||
|
switch (state) {
|
||||||
|
case MODE_STATE_PROPERTY:
|
||||||
|
return this.funcOrValue(this.context);
|
||||||
|
|
||||||
|
case MODE_STATE_INVOKE_FUNCTION:
|
||||||
|
return FunctionWrapper.apply(this.funcOrValue, this.args);
|
||||||
|
|
||||||
|
case MODE_STATE_CONST:
|
||||||
|
return this.funcOrValue;
|
||||||
|
|
||||||
|
case MODE_STATE_MARKER:
|
||||||
|
throw new BaseException('MODE_STATE_MARKER not implemented');
|
||||||
|
|
||||||
|
case MODE_STATE_INVOKE_METHOD:
|
||||||
|
throw new BaseException('MODE_STATE_INVOKE_METHOD not implemented');
|
||||||
|
|
||||||
|
case MODE_STATE_MAP:
|
||||||
|
throw new BaseException('MODE_STATE_MAP not implemented');
|
||||||
|
|
||||||
|
case MODE_STATE_LIST:
|
||||||
|
throw new BaseException('MODE_STATE_LIST not implemented');
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BaseException('DEFAULT not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArg(value, position:int) {
|
||||||
|
this.args[position] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContext(value) {
|
||||||
|
this.context = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The mode is divided into two parts. Which notification mechanism
|
// The mode is divided into two parts. Which notification mechanism
|
||||||
|
@ -154,7 +183,7 @@ const MODE_STATE_MARKER = 0x0000;
|
||||||
/// _context[_protoRecord.propname] => _getter(_context)
|
/// _context[_protoRecord.propname] => _getter(_context)
|
||||||
const MODE_STATE_PROPERTY = 0x0001;
|
const MODE_STATE_PROPERTY = 0x0001;
|
||||||
/// _context(_arguments)
|
/// _context(_arguments)
|
||||||
const MODE_STATE_INVOKE_CLOSURE = 0x0002;
|
const MODE_STATE_INVOKE_FUNCTION = 0x0002;
|
||||||
/// _getter(_context, _arguments)
|
/// _getter(_context, _arguments)
|
||||||
const MODE_STATE_INVOKE_METHOD = 0x0003;
|
const MODE_STATE_INVOKE_METHOD = 0x0003;
|
||||||
|
|
||||||
|
@ -163,7 +192,11 @@ const MODE_STATE_MAP = 0x0004;
|
||||||
/// _context is Array/List/Iterable => _previousValue = ListChangeRecord
|
/// _context is Array/List/Iterable => _previousValue = ListChangeRecord
|
||||||
const MODE_STATE_LIST = 0x0005;
|
const MODE_STATE_LIST = 0x0005;
|
||||||
|
|
||||||
|
/// _context is number/string
|
||||||
|
const MODE_STATE_CONST = 0x0006;
|
||||||
|
|
||||||
function isSame(a, b) {
|
function isSame(a, b) {
|
||||||
|
if (a instanceof String && b instanceof String) return a == b;
|
||||||
if (a === b) return true;
|
if (a === b) return true;
|
||||||
if ((a !== a) && (b !== b)) return true;
|
if ((a !== a) && (b !== b)) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {ProtoRecord, Record} from './record';
|
import {ProtoRecord, Record, PROTO_RECORD_CONST, PROTO_RECORD_FUNC, PROTO_RECORD_PROPERTY} from './record';
|
||||||
import {FIELD, IMPLEMENTS, isBlank, isPresent} from 'facade/lang';
|
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException} from 'facade/lang';
|
||||||
import {AST, AccessMember, ImplicitReceiver, AstVisitor, Binary, LiteralPrimitive} from './parser/ast';
|
import {ListWrapper} from 'facade/collection';
|
||||||
|
import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive, Binary} from './parser/ast';
|
||||||
|
|
||||||
export class ProtoWatchGroup {
|
export class ProtoWatchGroup {
|
||||||
@FIELD('headRecord:ProtoRecord')
|
@FIELD('headRecord:ProtoRecord')
|
||||||
|
@ -43,28 +44,40 @@ export class ProtoWatchGroup {
|
||||||
// but @Implements is not ready yet.
|
// but @Implements is not ready yet.
|
||||||
instantiate(dispatcher):WatchGroup {
|
instantiate(dispatcher):WatchGroup {
|
||||||
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
|
var watchGroup:WatchGroup = new WatchGroup(this, dispatcher);
|
||||||
var tail:Record = null;
|
|
||||||
var proto:ProtoRecord = null;
|
|
||||||
var prevRecord:Record = null;
|
|
||||||
|
|
||||||
if (this.headRecord !== null) {
|
if (this.headRecord !== null) {
|
||||||
watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord);
|
this._createRecords(watchGroup);
|
||||||
|
this._setDestination();
|
||||||
|
|
||||||
for (proto = this.headRecord.next; proto != null; proto = proto.next) {
|
}
|
||||||
|
return watchGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createRecords(watchGroup:WatchGroup) {
|
||||||
|
var tail, prevRecord;
|
||||||
|
watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord);
|
||||||
|
this.headRecord.recordInConstruction = watchGroup.headRecord;
|
||||||
|
|
||||||
|
for (var proto = this.headRecord.next; proto != null; proto = proto.next) {
|
||||||
prevRecord = tail;
|
prevRecord = tail;
|
||||||
|
|
||||||
tail = new Record(watchGroup, proto);
|
tail = new Record(watchGroup, proto);
|
||||||
|
proto.recordInConstruction = tail;
|
||||||
|
|
||||||
tail.prev = prevRecord;
|
tail.prev = prevRecord;
|
||||||
prevRecord.next = tail;
|
prevRecord.next = tail;
|
||||||
tail.checkPrev = prevRecord;
|
|
||||||
prevRecord.checkNext = tail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchGroup.tailRecord = tail;
|
watchGroup.tailRecord = tail;
|
||||||
}
|
}
|
||||||
|
|
||||||
return watchGroup;
|
_setDestination() {
|
||||||
|
for (var proto = this.headRecord; proto != null; proto = proto.next) {
|
||||||
|
if (proto.dest instanceof Destination) {
|
||||||
|
proto.recordInConstruction.dest = proto.dest.record.recordInConstruction;
|
||||||
|
}
|
||||||
|
proto.recordInConstruction = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WatchGroup {
|
export class WatchGroup {
|
||||||
|
@ -101,7 +114,8 @@ export class WatchGroup {
|
||||||
for (var record:Record = this.headRecord;
|
for (var record:Record = this.headRecord;
|
||||||
record != null;
|
record != null;
|
||||||
record = record.next) {
|
record = record.next) {
|
||||||
record.setContext(context);
|
|
||||||
|
record.updateContext(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +125,15 @@ export class WatchGroupDispatcher {
|
||||||
onRecordChange(record:Record, context) {}
|
onRecordChange(record:Record, context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//todo: vsavkin: Create Array and Context destinations?
|
||||||
|
class Destination {
|
||||||
|
constructor(record:ProtoRecord, position:int) {
|
||||||
|
this.record = record;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@IMPLEMENTS(AstVisitor)
|
@IMPLEMENTS(AstVisitor)
|
||||||
class ProtoRecordCreator {
|
class ProtoRecordCreator {
|
||||||
@FIELD('final protoWatchGroup:ProtoWatchGroup')
|
@FIELD('final protoWatchGroup:ProtoWatchGroup')
|
||||||
|
@ -122,7 +145,7 @@ class ProtoRecordCreator {
|
||||||
this.tailRecord = null;
|
this.tailRecord = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitImplicitReceiver(ast:ImplicitReceiver) {
|
visitImplicitReceiver(ast:ImplicitReceiver, args) {
|
||||||
//do nothing
|
//do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,16 +160,31 @@ class ProtoRecordCreator {
|
||||||
ast.right.visit(this);
|
ast.right.visit(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitAccessMember(ast:AccessMember) {
|
visitLiteralPrimitive(ast:LiteralPrimitive, dest) {
|
||||||
ast.receiver.visit(this);
|
this.add(this.construct(PROTO_RECORD_CONST, ast.value, 0, dest));
|
||||||
this.add(new ProtoRecord(this.protoWatchGroup, ast.name, null));
|
}
|
||||||
|
|
||||||
|
visitBinary(ast:Binary, dest) {
|
||||||
|
var record = this.construct(PROTO_RECORD_FUNC, _operationToFunction(ast.operation), 2, dest);
|
||||||
|
|
||||||
|
ast.left.visit(this, new Destination(record, 0));
|
||||||
|
ast.right.visit(this, new Destination(record, 1));
|
||||||
|
|
||||||
|
this.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAccessMember(ast:AccessMember, dest) {
|
||||||
|
var record = this.construct(PROTO_RECORD_PROPERTY, ast.getter, 0, dest);
|
||||||
|
ast.receiver.visit(this, new Destination(record, null));
|
||||||
|
this.add(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRecordsFromAST(ast:AST, memento){
|
createRecordsFromAST(ast:AST, memento){
|
||||||
ast.visit(this);
|
ast.visit(this, memento);
|
||||||
if (isPresent(this.tailRecord)) {
|
|
||||||
this.tailRecord.dispatchMemento = memento;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
construct(recordType, funcOrValue, arity, dest) {
|
||||||
|
return new ProtoRecord(this.protoWatchGroup, recordType, funcOrValue, arity, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(protoRecord:ProtoRecord) {
|
add(protoRecord:ProtoRecord) {
|
||||||
|
@ -159,3 +197,40 @@ class ProtoRecordCreator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _operationToFunction(operation:string):Function {
|
||||||
|
switch(operation) {
|
||||||
|
case '!' : return _operation_negate;
|
||||||
|
case '+' : return _operation_add;
|
||||||
|
case '-' : return _operation_subtract;
|
||||||
|
case '*' : return _operation_multiply;
|
||||||
|
case '/' : return _operation_divide;
|
||||||
|
case '%' : return _operation_remainder;
|
||||||
|
case '==' : return _operation_equals;
|
||||||
|
case '!=' : return _operation_not_equals;
|
||||||
|
case '<' : return _operation_less_then;
|
||||||
|
case '>' : return _operation_greater_then;
|
||||||
|
case '<=' : return _operation_less_or_equals_then;
|
||||||
|
case '>=' : return _operation_greater_or_equals_then;
|
||||||
|
case '&&' : return _operation_logical_and;
|
||||||
|
case '||' : return _operation_logical_or;
|
||||||
|
default: throw new BaseException(`Unsupported operation ${operation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _operation_negate(value) {return !value;}
|
||||||
|
function _operation_add(left, right) {return left + right;}
|
||||||
|
function _operation_subtract(left, right) {return left - right;}
|
||||||
|
function _operation_multiply(left, right) {return left * right;}
|
||||||
|
function _operation_divide(left, right) {return left / right;}
|
||||||
|
function _operation_remainder(left, right) {return left % right;}
|
||||||
|
function _operation_equals(left, right) {return left == right;}
|
||||||
|
function _operation_not_equals(left, right) {return left != right;}
|
||||||
|
function _operation_less_then(left, right) {return left < right;}
|
||||||
|
function _operation_greater_then(left, right) {return left > right;}
|
||||||
|
function _operation_less_or_equals_then(left, right) {return left <= right;}
|
||||||
|
function _operation_greater_or_equals_then(left, right) {return left >= right;}
|
||||||
|
function _operation_logical_and(left, right) {return left && right;}
|
||||||
|
function _operation_logical_or(left, right) {return left || right;}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {describe, it, xit, expect} from 'test_lib/test_lib';
|
import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
|
||||||
|
|
||||||
import {List, ListWrapper} from 'facade/collection';
|
import {List, ListWrapper} from 'facade/collection';
|
||||||
import {ImplicitReceiver, AccessMember} from 'change_detection/parser/ast';
|
import {Parser} from 'change_detection/parser/parser';
|
||||||
|
import {Lexer} from 'change_detection/parser/lexer';
|
||||||
import {ClosureMap} from 'change_detection/parser/closure_map';
|
import {ClosureMap} from 'change_detection/parser/closure_map';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -16,77 +17,107 @@ import {Record} from 'change_detection/record';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
function ast(exp:string) {
|
function ast(exp:string) {
|
||||||
var parts = exp.split(".");
|
var parser = new Parser(new Lexer(), new ClosureMap());
|
||||||
var cm = new ClosureMap();
|
return parser.parseBinding(exp);
|
||||||
return ListWrapper.reduce(parts, function (ast, fieldName) {
|
|
||||||
return new AccessMember(ast, fieldName, cm.getter(fieldName), cm.setter(fieldName));
|
|
||||||
}, new ImplicitReceiver());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('change_detection', function() {
|
function createChangeDetector(memo:string, exp:string, context = null) {
|
||||||
describe('ChangeDetection', function() {
|
|
||||||
it('should do simple watching', function() {
|
|
||||||
var person = new Person('misko', 38);
|
|
||||||
var pwg = new ProtoWatchGroup();
|
var pwg = new ProtoWatchGroup();
|
||||||
pwg.watch(ast('name'), 'name');
|
pwg.watch(ast(exp), memo, false);
|
||||||
pwg.watch(ast('age'), 'age');
|
|
||||||
var dispatcher = new LoggingDispatcher();
|
var dispatcher = new LoggingDispatcher();
|
||||||
var wg = pwg.instantiate(dispatcher);
|
var wg = pwg.instantiate(dispatcher);
|
||||||
wg.setContext(person);
|
wg.setContext(context);
|
||||||
|
|
||||||
var cd = new ChangeDetector(wg);
|
var cd = new ChangeDetector(wg);
|
||||||
|
|
||||||
|
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeWatch(memo:string, exp:string, context = null) {
|
||||||
|
var res = createChangeDetector(memo, exp, context);
|
||||||
|
res["changeDetector"].detectChanges();
|
||||||
|
return res["dispatcher"].log;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('change_detection', () => {
|
||||||
|
describe('ChangeDetection', () => {
|
||||||
|
it('should do simple watching', () => {
|
||||||
|
var person = new Person("misko");
|
||||||
|
|
||||||
|
var c = createChangeDetector('name', 'name', person);
|
||||||
|
var cd = c["changeDetector"];
|
||||||
|
var dispatcher = c["dispatcher"];
|
||||||
|
|
||||||
cd.detectChanges();
|
cd.detectChanges();
|
||||||
expect(dispatcher.log).toEqual(['name=misko', 'age=38']);
|
expect(dispatcher.log).toEqual(['name=misko']);
|
||||||
|
|
||||||
dispatcher.clear();
|
dispatcher.clear();
|
||||||
cd.detectChanges();
|
cd.detectChanges();
|
||||||
expect(dispatcher.log).toEqual([]);
|
expect(dispatcher.log).toEqual([]);
|
||||||
|
|
||||||
person.age = 1;
|
|
||||||
person.name = "Misko";
|
person.name = "Misko";
|
||||||
cd.detectChanges();
|
cd.detectChanges();
|
||||||
expect(dispatcher.log).toEqual(['name=Misko', 'age=1']);
|
expect(dispatcher.log).toEqual(['name=Misko']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should watch chained properties', function() {
|
it('should watch chained properties', () => {
|
||||||
var address = new Address('Grenoble');
|
var address = new Address('Grenoble');
|
||||||
var person = new Person('Victor', 36, address);
|
var person = new Person('Victor', address);
|
||||||
var pwg = new ProtoWatchGroup();
|
|
||||||
pwg.watch(ast('address.city'), 'address.city', false);
|
|
||||||
var dispatcher = new LoggingDispatcher();
|
|
||||||
var wg = pwg.instantiate(dispatcher);
|
|
||||||
wg.setContext(person);
|
|
||||||
|
|
||||||
var cd = new ChangeDetector(wg);
|
expect(executeWatch('address.city', 'address.city', person))
|
||||||
cd.detectChanges();
|
.toEqual(['address.city=Grenoble']);
|
||||||
expect(dispatcher.log).toEqual(['address.city=Grenoble']);
|
|
||||||
|
|
||||||
dispatcher.clear();
|
|
||||||
cd.detectChanges();
|
|
||||||
expect(dispatcher.log).toEqual([]);
|
|
||||||
|
|
||||||
address.city = 'Mountain View';
|
|
||||||
cd.detectChanges();
|
|
||||||
expect(dispatcher.log).toEqual(['address.city=Mountain View']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should watch literals", () => {
|
||||||
|
expect(executeWatch('const', '10')).toEqual(['const=10']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should watch binary operations", () => {
|
||||||
|
expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']);
|
||||||
|
expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']);
|
||||||
|
expect(executeWatch('exp', '10 / 2')).toEqual([`exp=${5.0}`]); //dart exp=5.0, js exp=5
|
||||||
|
expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']);
|
||||||
|
|
||||||
|
expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']);
|
||||||
|
expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class Person {
|
class Person {
|
||||||
constructor(name:string, age:number, address:Address = null) {
|
constructor(name:string, address:Address = null) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.age = age;
|
|
||||||
this.address = address;
|
this.address = address;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString():string {
|
toString():string {
|
||||||
var address = this.address == null ? '' : ' address=' + this.address.toString();
|
var address = this.address == null ? '' : ' address=' + this.address.toString();
|
||||||
|
|
||||||
return 'name=' + this.name +
|
return 'name=' + this.name + address;
|
||||||
' age=' + this.age.toString() +
|
|
||||||
address;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib';
|
import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib';
|
||||||
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
|
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
|
||||||
|
import {Record, ProtoRecord} from 'change_detection/record';
|
||||||
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
|
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
|
||||||
import {ProtoWatchGroup} from 'change_detection/watch_group';
|
import {ProtoWatchGroup} from 'change_detection/watch_group';
|
||||||
import {ChangeDetector} from 'change_detection/change_detector';
|
import {ChangeDetector} from 'change_detection/change_detector';
|
||||||
|
@ -94,42 +95,31 @@ export function main() {
|
||||||
createCollectDomNodesTestCases(true);
|
createCollectDomNodesTestCases(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create ElementInjectors', () => {
|
describe('react to watch group changes', function() {
|
||||||
it('should use the directives of the ProtoElementInjector', () => {
|
var view;
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), new ProtoWatchGroup());
|
beforeEach(() => {
|
||||||
pv.bindElement(new ProtoElementInjector(null, 1, [Directive]));
|
var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings);
|
||||||
|
var pv = new ProtoView(template, templateElementBinders(),
|
||||||
var view = pv.instantiate(null, null);
|
new ProtoWatchGroup(), false);
|
||||||
expect(view.elementInjectors.length).toBe(1);
|
view = pv.instantiate(null, null);
|
||||||
expect(view.elementInjectors[0].get(Directive) instanceof Directive).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the correct parent', () => {
|
it('should consume text node changes', () => {
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
var record = new Record(null, null);
|
||||||
new ProtoWatchGroup());
|
record.currentValue = 'Hello World!';
|
||||||
var protoParent = new ProtoElementInjector(null, 0, [Directive]);
|
view.onRecordChange(record , 0);
|
||||||
pv.bindElement(protoParent);
|
expect(view.textNodes[0].nodeValue).toEqual('Hello World!');
|
||||||
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.elementInjectors.length).toBe(2);
|
|
||||||
expect(view.elementInjectors[0].get(Directive) instanceof Directive).toBe(true);
|
|
||||||
expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collect root element injectors', () => {
|
it('should consume element binding changes', () => {
|
||||||
|
var elementWithBinding = view.bindElements[0];
|
||||||
it('should collect a single root element injector', () => {
|
expect(elementWithBinding.id).toEqual('');
|
||||||
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
var record = new Record(null, null);
|
||||||
new ProtoWatchGroup());
|
var memento = new ElementPropertyMemento(0, 'id');
|
||||||
var protoParent = new ProtoElementInjector(null, 0, [Directive]);
|
record.currentValue = 'foo';
|
||||||
pv.bindElement(protoParent);
|
view.onRecordChange(record, memento);
|
||||||
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
expect(elementWithBinding.id).toEqual('foo');
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
|
||||||
expect(view.rootElementInjectors.length).toBe(1);
|
|
||||||
expect(view.rootElementInjectors[0].get(Directive) instanceof Directive).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should collect multiple root element injectors', () => {
|
it('should collect multiple root element injectors', () => {
|
||||||
|
@ -138,10 +128,13 @@ export function main() {
|
||||||
pv.bindElement(new ProtoElementInjector(null, 1, [Directive]));
|
pv.bindElement(new ProtoElementInjector(null, 1, [Directive]));
|
||||||
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
|
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
|
||||||
|
|
||||||
var view = pv.instantiate(null, null);
|
expect(elInj.get(Directive).prop).toEqual('foo');
|
||||||
expect(view.rootElementInjectors.length).toBe(2);
|
var record = new Record(null, null);
|
||||||
expect(view.rootElementInjectors[0].get(Directive) instanceof Directive).toBe(true);
|
var memento = new DirectivePropertyMemento(1, 0, 'prop',
|
||||||
expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).toBe(true);
|
(o, v) => o.prop = v);
|
||||||
|
record.currentValue = 'bar';
|
||||||
|
view.onRecordChange(record, memento);
|
||||||
|
expect(elInj.get(Directive).prop).toEqual('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue