254 lines
7.3 KiB
JavaScript
Raw Normal View History

import {ProtoWatchGroup, WatchGroup} from './watch_group';
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
import {ListWrapper, MapWrapper} from 'facade/collection';
import {ClosureMap} from 'change_detection/parser/closure_map';
2014-09-26 11:20:08 -07:00
var _fresh = new Object();
export const PROTO_RECORD_CONST = 'const';
export const PROTO_RECORD_PURE_FUNCTION = 'func';
export const PROTO_RECORD_CLOSURE = 'closure';
export const PROTO_RECORD_FORMATTTER = 'formatter';
export const PROTO_RECORD_METHOD = 'method';
export const PROTO_RECORD_PROPERTY = 'property';
2014-09-30 15:50:20 -07:00
/**
* 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.
2014-09-30 15:50:20 -07:00
*/
2014-09-26 11:20:08 -07:00
export class ProtoRecord {
2014-09-30 15:50:20 -07:00
@FIELD('final watchGroup:wg.ProtoWatchGroup')
@FIELD('final context:Object')
@FIELD('final funcOrValue:Object')
@FIELD('final arity:int')
@FIELD('final dest')
2014-09-26 11:20:08 -07:00
@FIELD('next:ProtoRecord')
@FIELD('prev:ProtoRecord')
@FIELD('recordInConstruction:Record')
constructor(watchGroup:ProtoWatchGroup,
recordType:string,
funcOrValue,
arity:int,
dest) {
2014-09-26 11:20:08 -07:00
this.watchGroup = watchGroup;
this.recordType = recordType;
this.funcOrValue = funcOrValue;
this.arity = arity;
this.dest = dest;
2014-09-28 16:29:11 -07:00
this.next = null;
this.prev = null;
this.recordInConstruction = null;
}
2014-09-26 11:20:08 -07:00
}
2014-09-25 16:53:32 -07:00
/**
* Represents a Record for keeping track of changes. A change is a difference between previous
* and current value.
*
2014-09-25 16:53:32 -07:00
* By default changes are detected using dirty checking, but a notifier can be present which can
* notify the records of changes by means other than dirty checking. For example Object.observe
* or events on DOM elements.
*
* DESIGN NOTES:
* - No inheritance allowed so that code is monomorphic for performance.
2014-09-25 16:53:32 -07:00
* - Atomic watch operations
* - Defaults to dirty checking
* - Keep this object as lean as possible. (Lean in number of fields)
*/
export class Record {
2014-09-26 11:20:08 -07:00
@FIELD('final watchGroup:WatchGroup')
@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')
2014-09-26 11:20:08 -07:00
@FIELD('previousValue')
@FIELD('currentValue')
@FIELD('mode:int')
@FIELD('context')
@FIELD('funcOrValue')
@FIELD('args:List')
// Opaque data which will be the target of notification.
// 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, formatters:Map) {
2014-09-26 11:20:08 -07:00
this.watchGroup = watchGroup;
this.protoRecord = protoRecord;
2014-09-28 16:29:11 -07:00
this.next = null;
this.prev = null;
this.nextEnabled = null;
this.prevEnabled = null;
this.disabled = false;
this.dest = null;
2014-09-26 11:20:08 -07:00
this.previousValue = null;
this.currentValue = _fresh;
2014-09-25 16:53:32 -07:00
this.mode = null;
this.context = null;
this.funcOrValue = null;
this.args = null;
if (isBlank(protoRecord)) {
this.mode = MODE_STATE_MARKER;
return;
}
2014-11-11 18:36:44 -08:00
var type = protoRecord.recordType;
if (type === PROTO_RECORD_CONST) {
this.mode = MODE_STATE_CONST;
this.funcOrValue = protoRecord.funcOrValue;
2014-11-11 18:36:44 -08:00
} else if (type === PROTO_RECORD_PURE_FUNCTION) {
this.mode = MODE_STATE_INVOKE_PURE_FUNCTION;
this.funcOrValue = protoRecord.funcOrValue;
this.args = ListWrapper.createFixedSize(protoRecord.arity);
} else if (type === PROTO_RECORD_FORMATTTER) {
this.mode = MODE_STATE_INVOKE_PURE_FUNCTION;
this.funcOrValue = MapWrapper.get(formatters, protoRecord.funcOrValue);
this.args = ListWrapper.createFixedSize(protoRecord.arity);
2014-11-11 18:36:44 -08:00
} else if (type === PROTO_RECORD_METHOD) {
this.mode = MODE_STATE_INVOKE_METHOD;
this.funcOrValue = protoRecord.funcOrValue;
this.args = ListWrapper.createFixedSize(protoRecord.arity);
2014-11-11 18:36:44 -08:00
} else if (type === PROTO_RECORD_CLOSURE) {
this.mode = MODE_STATE_INVOKE_CLOSURE;
this.args = ListWrapper.createFixedSize(protoRecord.arity);
2014-11-11 18:36:44 -08:00
} else if (type === PROTO_RECORD_PROPERTY) {
this.mode = MODE_STATE_PROPERTY;
this.funcOrValue = protoRecord.funcOrValue;
2014-09-25 16:53:32 -07:00
}
}
static createMarker(wg:WatchGroup) {
var r = new Record(wg, null, null);
r.disabled = true;
return r;
}
check():boolean {
this.previousValue = this.currentValue;
this.currentValue = this._calculateNewValue();
if (isSame(this.previousValue, this.currentValue)) return false;
this._updateDestination();
return true;
}
_updateDestination() {
// todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
if (this.dest instanceof Record) {
if (isPresent(this.protoRecord.dest.position)) {
this.dest.updateArg(this.currentValue, this.protoRecord.dest.position);
} else {
this.dest.updateContext(this.currentValue);
}
2014-09-30 15:50:20 -07:00
} else {
this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dest);
2014-09-30 15:50:20 -07:00
}
}
_calculateNewValue() {
var state = this.mode;
switch (state) {
case MODE_STATE_PROPERTY:
return this.funcOrValue(this.context);
case MODE_STATE_INVOKE_METHOD:
return this.funcOrValue(this.context, this.args);
case MODE_STATE_INVOKE_CLOSURE:
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:
throw new BaseException('MODE_STATE_MARKER 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');
}
2014-09-25 16:53:32 -07:00
}
updateArg(value, position:int) {
this.args[position] = value;
this.watchGroup.enableRecord(this);
}
updateContext(value) {
this.context = value;
if (! this.isMarkerRecord) {
this.watchGroup.enableRecord(this);
}
}
get isMarkerRecord() {
return isBlank(this.protoRecord);
}
2014-09-25 16:53:32 -07:00
}
// The mode is divided into two parts. Which notification mechanism
2014-09-25 16:53:32 -07:00
// to use and which dereference mode to execute.
// We use dirty checking aka no notification
2014-09-26 11:20:08 -07:00
const MODE_MASK_NOTIFY = 0xFF00;
2014-09-25 16:53:32 -07:00
// Encodes the state of dereference
2014-09-26 11:20:08 -07:00
const MODE_MASK_STATE = 0x00FF;
2014-09-25 16:53:32 -07:00
2014-09-26 11:20:08 -07:00
const MODE_PLUGIN_DIRTY_CHECK = 0x0000;
const MODE_STATE_MARKER = 0x0000;
2014-09-25 16:53:32 -07:00
/// _context[_protoRecord.propname] => _getter(_context)
2014-09-26 11:20:08 -07:00
const MODE_STATE_PROPERTY = 0x0001;
const MODE_STATE_INVOKE_PURE_FUNCTION = 0x0002;
2014-09-26 11:20:08 -07:00
const MODE_STATE_INVOKE_METHOD = 0x0003;
const MODE_STATE_INVOKE_CLOSURE = 0x0004;
2014-09-25 16:53:32 -07:00
/// _context is Map => _previousValue is MapChangeRecord
const MODE_STATE_MAP = 0x0005;
2014-09-25 16:53:32 -07:00
/// _context is Array/List/Iterable => _previousValue = ListChangeRecord
const MODE_STATE_LIST = 0x0006;
2014-09-25 16:53:32 -07:00
/// _context is number/string
const MODE_STATE_CONST = 0x0007;
2014-09-25 16:53:32 -07:00
function isSame(a, b) {
if (a === b) return true;
if ((a !== a) && (b !== b)) return true;
return false;
}