feat(change_detection): added support for observable components and directives

This commit is contained in:
vsavkin 2015-08-21 14:45:38 -07:00
parent 85ec34d1d9
commit e8e430e630
11 changed files with 202 additions and 105 deletions

View File

@ -15,6 +15,8 @@ import {Locals} from './parser/locals';
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './constants';
import {wtfCreateScope, wtfLeave, WtfScopeFn} from '../profile/profile';
import {isObservable} from './observable_facade';
import {ON_PUSH_OBSERVE} from './constants';
var _scope_check: WtfScopeFn = wtfCreateScope(`ChangeDetector#check(ascii id, bool throwOnChange)`);
@ -44,7 +46,7 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
constructor(public id: string, public dispatcher: ChangeDispatcher,
public numberOfPropertyProtoRecords: number, public bindingTargets: BindingTarget[],
public directiveIndices: DirectiveIndex[], public modeOnHydrate: string) {
public directiveIndices: DirectiveIndex[], public strategy: string) {
this.ref = new ChangeDetectorRef(this);
}
@ -116,8 +118,13 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
// This method is not intended to be overridden. Subclasses should instead provide an
// implementation of `hydrateDirectives`.
hydrate(context: T, locals: Locals, directives: any, pipes: any): void {
this.mode = this.modeOnHydrate;
this.mode = ChangeDetectionUtil.changeDetectionMode(this.strategy);
this.context = context;
if (StringWrapper.equals(this.strategy, ON_PUSH_OBSERVE)) {
this.observeComponent(context);
}
this.locals = locals;
this.pipes = pipes;
this.hydrateDirectives(directives);
@ -133,7 +140,9 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
this.dehydrateDirectives(true);
// This is an experimental feature. Works only in Dart.
this.unsubsribeFromObservables();
if (StringWrapper.equals(this.strategy, ON_PUSH_OBSERVE)) {
this._unsubsribeFromObservables();
}
this.context = null;
this.locals = null;
@ -172,7 +181,8 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
}
}
private unsubsribeFromObservables(): void {
// This is an experimental feature. Works only in Dart.
private _unsubsribeFromObservables(): void {
if (isPresent(this.subscriptions)) {
for (var i = 0; i < this.subscriptions.length; ++i) {
var s = this.subscriptions[i];
@ -185,12 +195,9 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
}
// This is an experimental feature. Works only in Dart.
protected observe(value: any, index: number): any {
protected observeValue(value: any, index: number): any {
if (isObservable(value)) {
if (isBlank(this.subscriptions)) {
this.subscriptions = ListWrapper.createFixedSize(this.numberOfPropertyProtoRecords + 1);
this.streams = ListWrapper.createFixedSize(this.numberOfPropertyProtoRecords + 1);
}
this._createArrayToStoreObservables();
if (isBlank(this.subscriptions[index])) {
this.streams[index] = value.changes;
this.subscriptions[index] = value.changes.listen((_) => this.ref.requestCheck());
@ -203,6 +210,41 @@ export class AbstractChangeDetector<T> implements ChangeDetector {
return value;
}
// This is an experimental feature. Works only in Dart.
protected observeDirective(value: any, index: number): any {
if (isObservable(value)) {
this._createArrayToStoreObservables();
var arrayIndex = this.numberOfPropertyProtoRecords + index + 2; // +1 is component
this.streams[arrayIndex] = value.changes;
this.subscriptions[arrayIndex] = value.changes.listen((_) => this.ref.requestCheck());
}
return value;
}
// This is an experimental feature. Works only in Dart.
protected observeComponent(value: any): any {
if (isObservable(value)) {
this._createArrayToStoreObservables();
var index = this.numberOfPropertyProtoRecords + 1;
this.streams[index] = value.changes;
this.subscriptions[index] = value.changes.listen((_) => this.ref.requestCheck());
}
return value;
}
private _createArrayToStoreObservables(): void {
if (isBlank(this.subscriptions)) {
this.subscriptions = ListWrapper.createFixedSize(this.numberOfPropertyProtoRecords +
this.directiveIndices.length + 2);
this.streams = ListWrapper.createFixedSize(this.numberOfPropertyProtoRecords +
this.directiveIndices.length + 2);
}
}
protected getDirectiveFor(directives: any, index: number): any {
return directives.getDirectiveFor(this.directiveIndices[index]);
}
protected getDetectorFor(directives: any, index: number): ChangeDetector {
return directives.getDetectorFor(this.directiveIndices[index]);
}

View File

@ -8,6 +8,7 @@ import {DirectiveIndex, DirectiveRecord} from './directive_record';
import {ProtoRecord, RecordType} from './proto_record';
import {CodegenNameUtil, sanitizeName} from './codegen_name_util';
import {CodegenLogicUtil} from './codegen_logic_util';
import {codify} from './codegen_facade';
import {EventBinding} from './event_binding';
import {BindingTarget} from './binding_record';
import {ChangeDetectorGenConfig} from './interfaces';
@ -48,7 +49,7 @@ export class ChangeDetectorJITGenerator {
${ABSTRACT_CHANGE_DETECTOR}.call(
this, ${JSON.stringify(this.id)}, dispatcher, ${this.records.length},
${this._typeName}.gen_propertyBindingTargets, ${this._typeName}.gen_directiveIndices,
"${ChangeDetectionUtil.changeDetectionMode(this.changeDetectionStrategy)}");
${codify(this.changeDetectionStrategy)});
this.dehydrateDirectives(false);
}
@ -160,7 +161,7 @@ export class ChangeDetectorJITGenerator {
}
_maybeGenHydrateDirectives(): string {
var hydrateDirectivesCode = this._genHydrateDirectives();
var hydrateDirectivesCode = this._logic.genHydrateDirectives(this.directiveRecords);
var hydrateDetectorsCode = this._logic.genHydrateDetectors(this.directiveRecords);
if (!hydrateDirectivesCode && !hydrateDetectorsCode) return '';
return `${this._typeName}.prototype.hydrateDirectives = function(directives) {
@ -169,16 +170,6 @@ export class ChangeDetectorJITGenerator {
}`;
}
_genHydrateDirectives(): string {
var directiveFieldNames = this._names.getAllDirectiveNames();
var lines = ListWrapper.createFixedSize(directiveFieldNames.length);
for (var i = 0, iLen = directiveFieldNames.length; i < iLen; ++i) {
lines[i] = `${directiveFieldNames[i]} = directives.getDirectiveFor(
${this._names.getDirectivesAccessorName()}[${i}]);`;
}
return lines.join('\n');
}
_maybeGenCallOnAllChangesDone(): string {
var notifications = [];
var dirs = this.directiveRecords;

View File

@ -5,11 +5,7 @@ import {codify, combineGeneratedStrings, rawString} from './codegen_facade';
import {ProtoRecord, RecordType} from './proto_record';
import {BindingTarget} from './binding_record';
import {DirectiveRecord} from './directive_record';
/**
* This is an experimental feature. Works only in Dart.
*/
const ON_PUSH_OBSERVE = "ON_PUSH_OBSERVE";
import {ON_PUSH_OBSERVE} from './constants';
/**
* Class responsible for providing change detection logic for chagne detector classes.
@ -118,7 +114,7 @@ export class CodegenLogicUtil {
_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})`;
return `this.observeValue(${exp}, ${rec.selfIndex})`;
} else {
return exp;
}
@ -152,6 +148,24 @@ export class CodegenLogicUtil {
return combineGeneratedStrings(iVals);
}
genHydrateDirectives(directiveRecords: DirectiveRecord[]): string {
var res = [];
for (var i = 0; i < directiveRecords.length; ++i) {
var r = directiveRecords[i];
res.push(`${this._names.getDirectiveName(r.directiveIndex)} = ${this._genReadDirective(i)};`);
}
return res.join("\n");
}
private _genReadDirective(index: number) {
// This is an experimental feature. Works only in Dart.
if (StringWrapper.equals(this._changeDetection, ON_PUSH_OBSERVE)) {
return `this.observeDirective(this.getDirectiveFor(directives, ${index}), ${index})`;
} else {
return `this.getDirectiveFor(directives, ${index})`;
}
}
genHydrateDetectors(directiveRecords: DirectiveRecord[]): string {
var res = [];
for (var i = 0; i < directiveRecords.length; ++i) {

View File

@ -190,10 +190,6 @@ export class CodegenNameUtil {
return this._addFieldPrefix(`${this._sanitizedNames[idx]}_pipe`);
}
getAllDirectiveNames(): List<string> {
return ListWrapper.map(this.directiveRecords, d => this.getDirectiveName(d.directiveIndex));
}
getDirectiveName(d: DirectiveIndex): string {
return this._addFieldPrefix(`directive_${d.name}`);
}

View File

@ -37,4 +37,10 @@ export const DEFAULT: string = "DEFAULT";
export function isDefaultChangeDetectionStrategy(changeDetectionStrategy: string): boolean {
return isBlank(changeDetectionStrategy) || StringWrapper.equals(changeDetectionStrategy, DEFAULT);
}
}
/**
* This is an experimental feature. Works only in Dart.
*/
export const ON_PUSH_OBSERVE = "ON_PUSH_OBSERVE";

View File

@ -14,7 +14,7 @@ import {DirectiveRecord, DirectiveIndex} from './directive_record';
import {Locals} from './parser/locals';
import {ChangeDetectorGenConfig} from './interfaces';
import {ChangeDetectionUtil, SimpleChange} from './change_detection_util';
import {ON_PUSH_OBSERVE} from './constants';
import {ProtoRecord, RecordType} from './proto_record';
export class DynamicChangeDetector extends AbstractChangeDetector<any> {
@ -26,11 +26,11 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
constructor(id: string, dispatcher: any, numberOfPropertyProtoRecords: number,
propertyBindingTargets: BindingTarget[], directiveIndices: DirectiveIndex[],
modeOnHydrate: string, private records: ProtoRecord[],
strategy: string, private records: ProtoRecord[],
private eventBindings: EventBinding[], private directiveRecords: DirectiveRecord[],
private genConfig: ChangeDetectorGenConfig) {
super(id, dispatcher, numberOfPropertyProtoRecords, propertyBindingTargets, directiveIndices,
modeOnHydrate);
strategy);
var len = records.length + 1;
this.values = ListWrapper.createFixedSize(len);
this.localPipes = ListWrapper.createFixedSize(len);
@ -87,6 +87,13 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
hydrateDirectives(directives: any): void {
this.values[0] = this.context;
this.directives = directives;
if (StringWrapper.equals(this.strategy, ON_PUSH_OBSERVE)) {
for (var i = 0; i < this.directiveIndices.length; ++i) {
var index = this.directiveIndices[i];
super.observeDirective(directives.getDirectiveFor(index), i);
}
}
}
dehydrateDirectives(destroyPipes: boolean) {
@ -211,7 +218,11 @@ export class DynamicChangeDetector extends AbstractChangeDetector<any> {
return null;
}
var currValue = this.observe(this._calculateCurrValue(proto, values, locals), proto.selfIndex);
var currValue = this._calculateCurrValue(proto, values, locals);
if (StringWrapper.equals(this.strategy, ON_PUSH_OBSERVE)) {
super.observeValue(currValue, proto.selfIndex);
}
if (proto.shouldBeChecked()) {
var prevValue = this._readSelf(proto, values);
if (!isSame(prevValue, currValue)) {

View File

@ -52,8 +52,7 @@ export class DynamicProtoChangeDetector implements ProtoChangeDetector {
instantiate(dispatcher: any): ChangeDetector {
return new DynamicChangeDetector(
this.definition.id, dispatcher, this._propertyBindingRecords.length,
this._propertyBindingTargets, this._directiveIndices,
ChangeDetectionUtil.changeDetectionMode(this.definition.strategy),
this._propertyBindingTargets, this._directiveIndices, this.definition.strategy,
this._propertyBindingRecords, this._eventBindingRecords, this.definition.directiveRecords,
this.definition.genConfig);
}

View File

@ -10,6 +10,7 @@ import 'package:angular2/src/change_detection/proto_change_detector.dart';
import 'package:angular2/src/change_detection/proto_record.dart';
import 'package:angular2/src/change_detection/event_binding.dart';
import 'package:angular2/src/change_detection/binding_record.dart';
import 'package:angular2/src/change_detection/codegen_facade.dart' show codify;
import 'package:angular2/src/facade/lang.dart' show BaseException;
/// Responsible for generating change detector classes for Angular 2.
@ -74,7 +75,7 @@ class _CodegenState {
/// The name of the generated change detector class. This is an implementation
/// detail and should not be visible to users.
final String _changeDetectorTypeName;
final String _changeDetectionMode;
final String _changeDetectionStrategy;
final List<DirectiveRecord> _directiveRecords;
final List<ProtoRecord> _records;
final List<EventBinding> _eventBindings;
@ -87,16 +88,14 @@ class _CodegenState {
this._changeDetectorDefId,
this._contextTypeName,
this._changeDetectorTypeName,
String changeDetectionStrategy,
this._changeDetectionStrategy,
this._records,
this._propertyBindingTargets,
this._eventBindings,
this._directiveRecords,
this._logic,
this._names,
this._genConfig)
: _changeDetectionMode =
ChangeDetectionUtil.changeDetectionMode(changeDetectionStrategy);
this._genConfig);
factory _CodegenState(String typeName, String changeDetectorTypeName,
ChangeDetectorDefinition def) {
@ -130,7 +129,7 @@ class _CodegenState {
dispatcher, ${_records.length},
${_changeDetectorTypeName}.gen_propertyBindingTargets,
${_changeDetectorTypeName}.gen_directiveIndices,
'$_changeDetectionMode') {
${codify(_changeDetectionStrategy)}) {
dehydrateDirectives(false);
}
@ -248,7 +247,7 @@ class _CodegenState {
}
String _maybeGenHydrateDirectives() {
var hydrateDirectivesCode = _genHydrateDirectives();
var hydrateDirectivesCode = _logic.genHydrateDirectives(_directiveRecords);
var hydrateDetectorsCode = _logic.genHydrateDetectors(_directiveRecords);
if (hydrateDirectivesCode.isEmpty && hydrateDetectorsCode.isEmpty) {
return '';
@ -257,16 +256,6 @@ class _CodegenState {
'{ $hydrateDirectivesCode $hydrateDetectorsCode }';
}
String _genHydrateDirectives() {
var buf = new StringBuffer();
var directiveFieldNames = _names.getAllDirectiveNames();
for (var i = 0; i < directiveFieldNames.length; ++i) {
buf.writeln('${directiveFieldNames[i]} = directives.getDirectiveFor('
'${_names.getDirectivesAccessorName()}[$i]);');
}
return '$buf';
}
/// Generates calls to `onAllChangesDone` for all `Directive`s that request
/// them.
String _maybeGenCallOnAllChangesDone() {

View File

@ -12,6 +12,7 @@ import {
Parser,
ChangeDetectorGenConfig
} from 'angular2/src/change_detection/change_detection';
import {ON_PUSH_OBSERVE} from 'angular2/src/change_detection/constants';
import {reflector} from 'angular2/src/reflection/reflection';
import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities';
@ -106,11 +107,19 @@ export function getDefinition(id: string): TestDefinition {
[_DirectiveUpdating.basicRecords[0]], genConfig);
testDef = new TestDefinition(id, cdDef, null);
} else if (id == "onPushObserve") {
} else if (id == "onPushObserveBinding") {
var records = _createBindingRecords("a");
let cdDef = new ChangeDetectorDefinition(id, "ON_PUSH_OBSERVE", [], records, [], [], genConfig);
let cdDef = new ChangeDetectorDefinition(id, ON_PUSH_OBSERVE, [], records, [], [], genConfig);
testDef = new TestDefinition(id, cdDef, null);
} else if (id == "onPushObserveComponent") {
let cdDef = new ChangeDetectorDefinition(id, ON_PUSH_OBSERVE, [], [], [], [], genConfig);
testDef = new TestDefinition(id, cdDef, null);
} else if (id == "onPushObserveDirective") {
let cdDef = new ChangeDetectorDefinition(id, ON_PUSH_OBSERVE, [], [], [],
[_DirectiveUpdating.recordNoCallbacks], genConfig);
testDef = new TestDefinition(id, cdDef, null);
} else if (id == "updateElementProduction") {
var genConfig = new ChangeDetectorGenConfig(false, false, false);
var records = _createBindingRecords("name");
@ -118,7 +127,6 @@ export function getDefinition(id: string): TestDefinition {
testDef = new TestDefinition(id, cdDef, null);
}
if (isBlank(testDef)) {
throw `No ChangeDetectorDefinition for ${id} available. Please modify this file if necessary.`;
}
@ -144,7 +152,12 @@ export function getAllDefinitions(): List<TestDefinition> {
ListWrapper.concat(allDefs, StringMapWrapper.keys(_DirectiveUpdating.availableDefinitions));
allDefs = ListWrapper.concat(allDefs, _availableEventDefinitions);
allDefs = ListWrapper.concat(allDefs, _availableHostEventDefinitions);
allDefs = ListWrapper.concat(allDefs, ["onPushObserve", "updateElementProduction"]);
allDefs = ListWrapper.concat(allDefs, [
"onPushObserveBinding",
"onPushObserveComponent",
"onPushObserveDirective",
"updateElementProduction"
]);
return ListWrapper.map(allDefs, (id) => getDefinition(id));
}

View File

@ -781,64 +781,100 @@ export function main() {
});
if (IS_DART) {
it('should mark ON_PUSH_OBSERVE detectors as CHECK_ONCE when an observable fires an event',
fakeAsync(() => {
var context = new TestDirective();
context.a = createObservableModel();
describe('ON_PUSH_OBSERVE', () => {
it('should mark ON_PUSH_OBSERVE detectors as CHECK_ONCE when an observable fires an event',
fakeAsync(() => {
var context = new TestDirective();
context.a = createObservableModel();
var cd = _createWithoutHydrate('onPushObserve').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
var cd = _createWithoutHydrate('onPushObserveBinding').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
expect(cd.mode).toEqual(CHECKED);
expect(cd.mode).toEqual(CHECKED);
context.a.pushUpdate();
tick();
context.a.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECK_ONCE);
}));
expect(cd.mode).toEqual(CHECK_ONCE);
}));
it('should unsubscribe from an old observable when an object changes', fakeAsync(() => {
var originalModel = createObservableModel();
var context = new TestDirective();
context.a = originalModel;
it('should mark ON_PUSH_OBSERVE detectors as CHECK_ONCE when an observable context fires an event',
fakeAsync(() => {
var context = createObservableModel();
var cd = _createWithoutHydrate('onPushObserve').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
var cd = _createWithoutHydrate('onPushObserveComponent').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
context.a = createObservableModel();
cd.mode = CHECK_ONCE;
cd.detectChanges();
expect(cd.mode).toEqual(CHECKED);
// Updating this model will not reenable the detector. This model is not longer
// used.
originalModel.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECKED);
}));
context.pushUpdate();
tick();
it('should unsubscribe from observables when dehydrating', fakeAsync(() => {
var originalModel = createObservableModel();
var context = new TestDirective();
context.a = originalModel;
expect(cd.mode).toEqual(CHECK_ONCE);
}));
var cd = _createWithoutHydrate('onPushObserve').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
it('should mark ON_PUSH_OBSERVE detectors as CHECK_ONCE when an observable directive fires an event',
fakeAsync(() => {
var dir = createObservableModel();
var directives = new FakeDirectives([dir], []);
cd.dehydrate();
var cd = _createWithoutHydrate('onPushObserveDirective').changeDetector;
cd.hydrate(_DEFAULT_CONTEXT, null, directives, null);
cd.detectChanges();
context.a = "not an observable model";
cd.hydrate(context, null, directives, null);
cd.detectChanges();
expect(cd.mode).toEqual(CHECKED);
// Updating this model will not reenable the detector. This model is not longer
// used.
originalModel.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECKED);
}));
dir.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECK_ONCE);
}));
it('should unsubscribe from an old observable when an object changes',
fakeAsync(() => {
var originalModel = createObservableModel();
var context = new TestDirective();
context.a = originalModel;
var cd = _createWithoutHydrate('onPushObserveBinding').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
context.a = createObservableModel();
cd.mode = CHECK_ONCE;
cd.detectChanges();
// Updating this model will not reenable the detector. This model is not longer
// used.
originalModel.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECKED);
}));
it('should unsubscribe from observables when dehydrating', fakeAsync(() => {
var originalModel = createObservableModel();
var context = new TestDirective();
context.a = originalModel;
var cd = _createWithoutHydrate('onPushObserveBinding').changeDetector;
cd.hydrate(context, null, directives, null);
cd.detectChanges();
cd.dehydrate();
context.a = "not an observable model";
cd.hydrate(context, null, directives, null);
cd.detectChanges();
// Updating this model will not reenable the detector. This model is not longer
// used.
originalModel.pushUpdate();
tick();
expect(cd.mode).toEqual(CHECKED);
}));
});
}
});
});

View File

@ -29,7 +29,7 @@ class _MyComponent_ChangeDetector0
_MyComponent_ChangeDetector0(dispatcher) : super(
"MyComponent_comp_0", dispatcher, 2,
_MyComponent_ChangeDetector0.gen_propertyBindingTargets,
_MyComponent_ChangeDetector0.gen_directiveIndices, 'ALWAYS_CHECK') {
_MyComponent_ChangeDetector0.gen_directiveIndices,null) {
dehydrateDirectives(false);
}