feat(change_detection): added an experimental support for observables

This commit is contained in:
vsavkin
2015-08-17 15:28:37 -07:00
committed by Victor Savkin
parent ed81cb94b0
commit cbfc9cb344
18 changed files with 243 additions and 44 deletions

View File

@ -1,4 +1,4 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {ChangeDetectionUtil} from './change_detection_util';
import {ChangeDetectorRef} from './change_detector_ref';
@ -13,8 +13,9 @@ import {
import {ProtoRecord} from './proto_record';
import {BindingRecord} from './binding_record';
import {Locals} from './parser/locals';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH} from './constants';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './constants';
import {wtfCreateScope, wtfLeave, WtfScopeFn} from '../profile/profile';
import {isObservable} from './observable_facade';
var _scope_check: WtfScopeFn = wtfCreateScope(`ChangeDetector#check(ascii id, bool throwOnChange)`);
@ -42,6 +43,10 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
firstProtoInCurrentBinding: number;
protos: List<ProtoRecord>;
// This is an experimental feature. Works only in Dart.
subscriptions: any[];
streams: any[];
constructor(public id: string, dispatcher: ChangeDispatcher, protos: List<ProtoRecord>,
directiveRecords: List<DirectiveRecord>, public modeOnHydrate: string) {
this.ref = new ChangeDetectorRef(this);
@ -79,13 +84,14 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
checkNoChanges(): void { throw new BaseException("Not implemented"); }
runDetectChanges(throwOnChange: boolean): void {
if (this.mode === DETACHED || this.mode === CHECKED) return;
if (StringWrapper.equals(this.mode, DETACHED) || StringWrapper.equals(this.mode, CHECKED))
return;
var s = _scope_check(this.id, throwOnChange);
this.detectChangesInRecords(throwOnChange);
this._detectChangesInLightDomChildren(throwOnChange);
if (throwOnChange === false) this.callOnAllChangesDone();
this._detectChangesInShadowDomChildren(throwOnChange);
if (this.mode === CHECK_ONCE) this.mode = CHECKED;
if (StringWrapper.equals(this.mode, CHECK_ONCE)) this.mode = CHECKED;
wtfLeave(s);
}
@ -132,6 +138,10 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
// implementation of `dehydrateDirectives`.
dehydrate(): void {
this.dehydrateDirectives(true);
// This is an experimental feature. Works only in Dart.
this.unsubsribeFromObservables();
this.context = null;
this.locals = null;
this.pipes = null;
@ -163,12 +173,43 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
markPathToRootAsCheckOnce(): void {
var c: ChangeDetector = this;
while (isPresent(c) && c.mode != DETACHED) {
if (c.mode === CHECKED) c.mode = CHECK_ONCE;
while (isPresent(c) && !StringWrapper.equals(c.mode, DETACHED)) {
if (StringWrapper.equals(c.mode, CHECKED)) c.mode = CHECK_ONCE;
c = c.parent;
}
}
private unsubsribeFromObservables(): void {
if (isPresent(this.subscriptions)) {
for (var i = 0; i < this.subscriptions.length; ++i) {
var s = this.subscriptions[i];
if (isPresent(this.subscriptions[i])) {
s.cancel();
this.subscriptions[i] = null;
}
}
}
}
// This is an experimental feature. Works only in Dart.
protected observe(value: any, index: number): any {
if (isObservable(value)) {
if (isBlank(this.subscriptions)) {
this.subscriptions = ListWrapper.createFixedSize(this.protos.length + 1);
this.streams = ListWrapper.createFixedSize(this.protos.length + 1);
}
if (isBlank(this.subscriptions[index])) {
this.streams[index] = value.changes;
this.subscriptions[index] = value.changes.listen((_) => this.ref.requestCheck());
} else if (this.streams[index] !== value.changes) {
this.subscriptions[index].cancel();
this.streams[index] = value.changes;
this.subscriptions[index] = value.changes.listen((_) => this.ref.requestCheck());
}
}
return value;
}
protected notifyDispatcher(value: any): void {
this.dispatcher.notifyOnBinding(this._currentBinding(), value);
}

View File

@ -23,8 +23,8 @@ export class BindingRecord {
return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange;
}
isOnPushChangeDetection(): boolean {
return isPresent(this.directiveRecord) && this.directiveRecord.isOnPushChangeDetection();
isDefaultChangeDetection(): boolean {
return isBlank(this.directiveRecord) || this.directiveRecord.isDefaultChangeDetection();
}
isDirective(): boolean { return this.mode === DIRECTIVE; }

View File

@ -35,7 +35,7 @@ export class ChangeDetectorJITGenerator {
public directiveRecords: List<any>, private generateCheckNoChanges: boolean) {
this._names =
new CodegenNameUtil(this.records, this.eventBindings, this.directiveRecords, UTIL);
this._logic = new CodegenLogicUtil(this._names, UTIL);
this._logic = new CodegenLogicUtil(this._names, UTIL, changeDetectionStrategy);
this._typeName = sanitizeName(`ChangeDetector_${this.id}`);
}
@ -116,10 +116,10 @@ export class ChangeDetectorJITGenerator {
_genMarkPathToRootAsCheckOnce(r: ProtoRecord): string {
var br = r.bindingRecord;
if (br.isOnPushChangeDetection()) {
return `${this._names.getDetectorName(br.directiveRecord.directiveIndex)}.markPathToRootAsCheckOnce();`;
} else {
if (br.isDefaultChangeDetection()) {
return "";
} else {
return `${this._names.getDetectorName(br.directiveRecord.directiveIndex)}.markPathToRootAsCheckOnce();`;
}
}
@ -369,7 +369,7 @@ export class ChangeDetectorJITGenerator {
_genNotifyOnPushDetectors(r: ProtoRecord): string {
var br = r.bindingRecord;
if (!r.lastInDirective || !br.isOnPushChangeDetection()) return "";
if (!r.lastInDirective || br.isDefaultChangeDetection()) return "";
var retVal = `
if(${IS_CHANGED_LOCAL}) {
${this._names.getDetectorName(br.directiveRecord.directiveIndex)}.markAsCheckOnce();

View File

@ -1,7 +1,20 @@
import {CONST_EXPR, isPresent, isBlank, BaseException, Type} from 'angular2/src/facade/lang';
import {
CONST_EXPR,
isPresent,
isBlank,
BaseException,
Type,
StringWrapper
} from 'angular2/src/facade/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {ProtoRecord} from './proto_record';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH} from './constants';
import {
CHECK_ALWAYS,
CHECK_ONCE,
CHECKED,
DETACHED,
isDefaultChangeDetectionStrategy
} from './constants';
import {implementsOnDestroy} from './pipe_lifecycle_reflector';
@ -166,7 +179,7 @@ export class ChangeDetectionUtil {
}
static changeDetectionMode(strategy: string): string {
return strategy == ON_PUSH ? CHECK_ONCE : CHECK_ALWAYS;
return isDefaultChangeDetectionStrategy(strategy) ? CHECK_ALWAYS : CHECK_ONCE;
}
static simpleChange(previousValue: any, currentValue: any): SimpleChange {

View File

@ -1,14 +1,20 @@
import {ListWrapper} from 'angular2/src/facade/collection';
import {BaseException, Json} from 'angular2/src/facade/lang';
import {BaseException, Json, StringWrapper} from 'angular2/src/facade/lang';
import {CodegenNameUtil} from './codegen_name_util';
import {codify, combineGeneratedStrings, rawString} from './codegen_facade';
import {ProtoRecord, RecordType} from './proto_record';
/**
* This is an experimental feature. Works only in Dart.
*/
const ON_PUSH_OBSERVE = "ON_PUSH_OBSERVE";
/**
* Class responsible for providing change detection logic for chagne detector classes.
*/
export class CodegenLogicUtil {
constructor(private _names: CodegenNameUtil, private _utilName: string) {}
constructor(private _names: CodegenNameUtil, private _utilName: string,
private _changeDetection: string) {}
/**
* Generates a statement which updates the local variable representing `protoRec` with the current
@ -46,11 +52,13 @@ export class CodegenLogicUtil {
break;
case RecordType.PROPERTY_READ:
rhs = `${context}.${protoRec.name}`;
rhs = this._observe(`${context}.${protoRec.name}`, protoRec);
break;
case RecordType.SAFE_PROPERTY:
rhs = `${this._utilName}.isValueBlank(${context}) ? null : ${context}.${protoRec.name}`;
var read = this._observe(`${context}.${protoRec.name}`, protoRec);
rhs =
`${this._utilName}.isValueBlank(${context}) ? null : ${this._observe(read, protoRec)}`;
break;
case RecordType.PROPERTY_WRITE:
@ -58,16 +66,17 @@ export class CodegenLogicUtil {
break;
case RecordType.LOCAL:
rhs = `${localsAccessor}.get(${rawString(protoRec.name)})`;
rhs = this._observe(`${localsAccessor}.get(${rawString(protoRec.name)})`, protoRec);
break;
case RecordType.INVOKE_METHOD:
rhs = `${context}.${protoRec.name}(${argString})`;
rhs = this._observe(`${context}.${protoRec.name}(${argString})`, protoRec);
break;
case RecordType.SAFE_INVOKE_METHOD:
var invoke = `${context}.${protoRec.name}(${argString})`;
rhs =
`${this._utilName}.isValueBlank(${context}) ? null : ${context}.${protoRec.name}(${argString})`;
`${this._utilName}.isValueBlank(${context}) ? null : ${this._observe(invoke, protoRec)}`;
break;
case RecordType.INVOKE_CLOSURE:
@ -87,7 +96,7 @@ export class CodegenLogicUtil {
break;
case RecordType.KEYED_READ:
rhs = `${context}[${getLocalName(protoRec.args[0])}]`;
rhs = this._observe(`${context}[${getLocalName(protoRec.args[0])}]`, protoRec);
break;
case RecordType.KEYED_WRITE:
@ -104,6 +113,14 @@ export class CodegenLogicUtil {
return `${getLocalName(protoRec.selfIndex)} = ${rhs};`;
}
_observe(exp: string, rec: ProtoRecord): string {
// This is an experimental feature. Works only in Dart.
if (StringWrapper.equals(this._changeDetection, ON_PUSH_OBSERVE)) {
return `this.observe(${exp}, ${rec.selfIndex})`;
} else {
return exp;
}
}
_genInterpolation(protoRec: ProtoRecord): string {
var iVals = [];

View File

@ -124,7 +124,7 @@ export class CodegenNameUtil {
MapWrapper.forEach(this._sanitizedEventNames, (names, eb) => {
for (var i = 0; i < names.length; ++i) {
if (i !== CONTEXT_INDEX) {
res.push(this.getEventLocalName(eb, i));
res.push(`${this.getEventLocalName(eb, i)}`);
}
}
});
@ -155,7 +155,7 @@ export class CodegenNameUtil {
for (var j = 0, jLen = this.directiveRecords.length; j < jLen; ++j) {
var dRec = this.directiveRecords[j];
fieldList.push(this.getDirectiveName(dRec.directiveIndex));
if (dRec.isOnPushChangeDetection()) {
if (!dRec.isDefaultChangeDetection()) {
fieldList.push(this.getDetectorName(dRec.directiveIndex));
}
}
@ -202,7 +202,7 @@ export class CodegenNameUtil {
getAllDetectorNames(): List<string> {
return ListWrapper.map(
ListWrapper.filter(this.directiveRecords, r => r.isOnPushChangeDetection()),
ListWrapper.filter(this.directiveRecords, r => !r.isDefaultChangeDetection()),
(d) => this.getDetectorName(d.directiveIndex));
}

View File

@ -1,4 +1,5 @@
// TODO:vsavkin Use enums after switching to TypeScript
import {StringWrapper, normalizeBool, isBlank} from 'angular2/src/facade/lang';
/**
* CHECK_ONCE means that after calling detectChanges the mode of the change detector
@ -33,3 +34,7 @@ export const ON_PUSH: string = "ON_PUSH";
* DEFAULT means that the change detector's mode will be set to CHECK_ALWAYS during hydration.
*/
export const DEFAULT: string = "DEFAULT";
export function isDefaultChangeDetectionStrategy(changeDetectionStrategy: string): boolean {
return isBlank(changeDetectionStrategy) || StringWrapper.equals(changeDetectionStrategy, DEFAULT);
}

View File

@ -1,5 +1,5 @@
import {ON_PUSH} from './constants';
import {StringWrapper, normalizeBool} from 'angular2/src/facade/lang';
import {StringWrapper, normalizeBool, isBlank} from 'angular2/src/facade/lang';
import {isDefaultChangeDetectionStrategy} from './constants';
export class DirectiveIndex {
constructor(public elementIndex: number, public directiveIndex: number) {}
@ -32,5 +32,7 @@ export class DirectiveRecord {
this.changeDetection = changeDetection;
}
isOnPushChangeDetection(): boolean { return StringWrapper.equals(this.changeDetection, ON_PUSH); }
isDefaultChangeDetection(): boolean {
return isDefaultChangeDetectionStrategy(this.changeDetection);
}
}

View File

@ -1,4 +1,10 @@
import {isPresent, isBlank, BaseException, FunctionWrapper} from 'angular2/src/facade/lang';
import {
isPresent,
isBlank,
BaseException,
FunctionWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {AbstractChangeDetector} from './abstract_change_detector';
@ -64,7 +70,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
}
_markPathAsCheckOnce(proto: ProtoRecord): void {
if (proto.bindingRecord.isOnPushChangeDetection()) {
if (!proto.bindingRecord.isDefaultChangeDetection()) {
var dir = proto.bindingRecord.directiveRecord;
this._getDetectorFor(dir.directiveIndex).markPathToRootAsCheckOnce();
}
@ -136,7 +142,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
if (proto.lastInDirective) {
changes = null;
if (isChanged && bindingRecord.isOnPushChangeDetection()) {
if (isChanged && !bindingRecord.isDefaultChangeDetection()) {
this._getDetectorFor(directiveRecord.directiveIndex).markAsCheckOnce();
}
@ -198,7 +204,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
return null;
}
var currValue = this._calculateCurrValue(proto, values, locals);
var currValue = this.observe(this._calculateCurrValue(proto, values, locals), proto.selfIndex);
if (proto.shouldBeChecked()) {
var prevValue = this._readSelf(proto, values);
if (!isSame(prevValue, currValue)) {

View File

@ -0,0 +1,3 @@
import 'package:observe/observe.dart';
bool isObservable(value) => value is Observable;

View File

@ -0,0 +1,3 @@
export function isObservable(value: any): boolean {
return false;
}

View File

@ -48,10 +48,8 @@ class ObservableListDiff extends DefaultIterableDiffer {
return super.diff(collection);
// No updates has been registered.
// Returning this tells change detection that object has not change,
// so it should NOT update the binding.
} else {
return this;
return null;
}
}
}

View File

@ -100,7 +100,7 @@ class _CodegenState {
var protoRecords = createPropertyRecords(def);
var eventBindings = createEventRecords(def);
var names = new CodegenNameUtil(protoRecords, eventBindings, def.directiveRecords, _UTIL);
var logic = new CodegenLogicUtil(names, _UTIL);
var logic = new CodegenLogicUtil(names, _UTIL, def.strategy);
return new _CodegenState._(
def.id,
typeName,
@ -193,7 +193,7 @@ class _CodegenState {
String _genMarkPathToRootAsCheckOnce(ProtoRecord r) {
var br = r.bindingRecord;
if (br.isOnPushChangeDetection()) {
if (!br.isDefaultChangeDetection()) {
return "${_names.getDetectorName(br.directiveRecord.directiveIndex)}.markPathToRootAsCheckOnce();";
} else {
return "";
@ -469,7 +469,7 @@ class _CodegenState {
String _genNotifyOnPushDetectors(ProtoRecord r) {
var br = r.bindingRecord;
if (!r.lastInDirective || !br.isOnPushChangeDetection()) return '';
if (!r.lastInDirective || br.isDefaultChangeDetection()) return '';
return '''
if($_IS_CHANGED_LOCAL) {
${_names.getDetectorName(br.directiveRecord.directiveIndex)}.markAsCheckOnce();