feat(change_detection): updated change detection to update directive directly, without the dispatcher

This commit is contained in:
vsavkin 2015-04-01 15:49:14 -07:00
parent 50098767fc
commit 69c3bff086
8 changed files with 322 additions and 177 deletions

View File

@ -5,6 +5,6 @@ class ChangeDetectorJITGenerator {
} }
generate() { generate() {
throw "Not supported in Dart"; throw "Jit Change Detection is not supported in Dart";
} }
} }

View File

@ -76,16 +76,23 @@ function pipeOnDestroyTemplate(pipeNames:List) {
return pipeNames.map((p) => `${p}.onDestroy()`).join("\n"); return pipeNames.map((p) => `${p}.onDestroy()`).join("\n");
} }
function hydrateTemplate(type:string, mode:string, fieldsDefinitions:string, pipeOnDestroy:string):string { function hydrateTemplate(type:string, mode:string, fieldDefinitions:string, pipeOnDestroy:string,
directiveFieldNames:List<String>):string {
var directiveInit = "";
for(var i = 0; i < directiveFieldNames.length; ++i) {
directiveInit += `${directiveFieldNames[i]} = this.directiveMementos[${i}].directive(directives);\n`;
}
return ` return `
${type}.prototype.hydrate = function(context, locals) { ${type}.prototype.hydrate = function(context, locals, directives) {
${MODE_ACCESSOR} = "${mode}"; ${MODE_ACCESSOR} = "${mode}";
${CONTEXT_ACCESSOR} = context; ${CONTEXT_ACCESSOR} = context;
${LOCALS_ACCESSOR} = locals; ${LOCALS_ACCESSOR} = locals;
${directiveInit}
} }
${type}.prototype.dehydrate = function() { ${type}.prototype.dehydrate = function() {
${pipeOnDestroy} ${pipeOnDestroy}
${fieldsDefinitions} ${fieldDefinitions}
${LOCALS_ACCESSOR} = null; ${LOCALS_ACCESSOR} = null;
} }
${type}.prototype.hydrated = function() { ${type}.prototype.hydrated = function() {
@ -110,8 +117,8 @@ ${type}.prototype.callOnAllChangesDone = function() {
`; `;
} }
function onAllChangesDoneTemplate(index:number):string { function onAllChangesDoneTemplate(directive:string):string {
return `${DISPATCHER_ACCESSOR}.onAllChangesDone(${MEMENTOS_ACCESSOR}[${index}]);`; return `${directive}.onAllChangesDone();`;
} }
@ -130,7 +137,7 @@ ${records}
} }
function pipeCheckTemplate(protoIndex:number, context:string, bindingPropagationConfig:string, pipe:string, pipeType:string, function pipeCheckTemplate(protoIndex:number, context:string, bindingPropagationConfig:string, pipe:string, pipeType:string,
oldValue:string, newValue:string, change:string, invokeMementoAndAddChange:string, oldValue:string, newValue:string, change:string, invokeMemento:string,
addToChanges, lastInDirective:string):string{ addToChanges, lastInDirective:string):string{
return ` return `
${CURRENT_PROTO} = ${PROTOS_ACCESSOR}[${protoIndex}]; ${CURRENT_PROTO} = ${PROTOS_ACCESSOR}[${protoIndex}];
@ -144,7 +151,7 @@ if (${pipe} === ${UTIL}.unitialized()) {
${newValue} = ${pipe}.transform(${context}); ${newValue} = ${pipe}.transform(${context});
if (! ${UTIL}.noChangeMarker(${newValue})) { if (! ${UTIL}.noChangeMarker(${newValue})) {
${change} = true; ${change} = true;
${invokeMementoAndAddChange} ${invokeMemento}
${addToChanges} ${addToChanges}
${oldValue} = ${newValue}; ${oldValue} = ${newValue};
} }
@ -153,13 +160,13 @@ ${lastInDirective}
} }
function referenceCheckTemplate(protoIndex:number, assignment:string, oldValue:string, newValue:string, change:string, function referenceCheckTemplate(protoIndex:number, assignment:string, oldValue:string, newValue:string, change:string,
invokeMementoAndAddChange:string, addToChanges:string, lastInDirective:string):string { invokeMemento:string, addToChanges:string, lastInDirective:string):string {
return ` return `
${CURRENT_PROTO} = ${PROTOS_ACCESSOR}[${protoIndex}]; ${CURRENT_PROTO} = ${PROTOS_ACCESSOR}[${protoIndex}];
${assignment} ${assignment}
if (${newValue} !== ${oldValue} || (${newValue} !== ${newValue}) && (${oldValue} !== ${oldValue})) { if (${newValue} !== ${oldValue} || (${newValue} !== ${newValue}) && (${oldValue} !== ${oldValue})) {
${change} = true; ${change} = true;
${invokeMementoAndAddChange} ${invokeMemento}
${addToChanges} ${addToChanges}
${oldValue} = ${newValue}; ${oldValue} = ${newValue};
} }
@ -196,20 +203,26 @@ function addToChangesTemplate(oldValue:string, newValue:string):string {
return `${CHANGES_LOCAL} = ${UTIL}.addChange(${CHANGES_LOCAL}, ${CURRENT_PROTO}.bindingMemento, ${UTIL}.simpleChange(${oldValue}, ${newValue}));`; return `${CHANGES_LOCAL} = ${UTIL}.addChange(${CHANGES_LOCAL}, ${CURRENT_PROTO}.bindingMemento, ${UTIL}.simpleChange(${oldValue}, ${newValue}));`;
} }
function invokeBindingMemento(oldValue:string, newValue:string):string { function updateDirectiveTemplate(oldValue:string, newValue:string, directiveProperty:string):string {
return ` return `
if(throwOnChange) ${UTIL}.throwOnChange(${CURRENT_PROTO}, ${UTIL}.simpleChange(${oldValue}, ${newValue})); if(throwOnChange) ${UTIL}.throwOnChange(${CURRENT_PROTO}, ${UTIL}.simpleChange(${oldValue}, ${newValue}));
${directiveProperty} = ${newValue};
`;
}
function updateElementTemplate(oldValue:string, newValue:string):string {
return `
if(throwOnChange) ${UTIL}.throwOnChange(${CURRENT_PROTO}, ${UTIL}.simpleChange(${oldValue}, ${newValue}));
${DISPATCHER_ACCESSOR}.invokeMementoFor(${CURRENT_PROTO}.bindingMemento, ${newValue}); ${DISPATCHER_ACCESSOR}.invokeMementoFor(${CURRENT_PROTO}.bindingMemento, ${newValue});
`; `;
} }
function lastInDirectiveTemplate(protoIndex:number):string{ function notifyOnChangesTemplate(directive:string):string{
return ` return `
if (${CHANGES_LOCAL}) { if(${CHANGES_LOCAL}) {
${DISPATCHER_ACCESSOR}.onChange(${PROTOS_ACCESSOR}[${protoIndex}].directiveMemento, ${CHANGES_LOCAL}); ${directive}.onChange(${CHANGES_LOCAL});
${CHANGES_LOCAL} = null;
} }
${CHANGES_LOCAL} = null;
`; `;
} }
@ -271,13 +284,22 @@ export class ChangeDetectorJITGenerator {
genHydrate():string { genHydrate():string {
var mode = ChangeDetectionUtil.changeDetectionMode(this.changeDetectionStrategy); var mode = ChangeDetectionUtil.changeDetectionMode(this.changeDetectionStrategy);
return hydrateTemplate(this.typeName, mode, this.genFieldDefinitions(), return hydrateTemplate(this.typeName, mode, this.genFieldDefinitions(),
pipeOnDestroyTemplate(this.getNonNullPipeNames())); pipeOnDestroyTemplate(this.getNonNullPipeNames()), this.getDirectiveFieldNames());
}
getDirectiveFieldNames():List<string> {
return this.directiveMementos.map((d) => this.getDirective(d));
}
getDirective(memento) {
return `this.directive_${memento.name}`;
} }
genFieldDefinitions() { genFieldDefinitions() {
var fields = []; var fields = [];
fields = fields.concat(this.fieldNames); fields = fields.concat(this.fieldNames);
fields = fields.concat(this.getNonNullPipeNames()); fields = fields.concat(this.getNonNullPipeNames());
fields = fields.concat(this.getDirectiveFieldNames());
return fieldDefinitionsTemplate(fields); return fieldDefinitionsTemplate(fields);
} }
@ -303,7 +325,8 @@ export class ChangeDetectorJITGenerator {
for (var i = mementos.length - 1; i >= 0; --i) { for (var i = mementos.length - 1; i >= 0; --i) {
var memento = mementos[i]; var memento = mementos[i];
if (memento.callOnAllChangesDone) { if (memento.callOnAllChangesDone) {
notifications.push(onAllChangesDoneTemplate(i)); var directive = `this.directive_${memento.name}`;
notifications.push(onAllChangesDoneTemplate(directive));
} }
} }
@ -340,9 +363,9 @@ export class ChangeDetectorJITGenerator {
var pipe = this.pipeNames[r.selfIndex]; var pipe = this.pipeNames[r.selfIndex];
var bpc = r.mode === RECORD_TYPE_BINDING_PIPE ? "this.bindingPropagationConfig" : "null"; var bpc = r.mode === RECORD_TYPE_BINDING_PIPE ? "this.bindingPropagationConfig" : "null";
var invokeMemento = this.getInvokeMementoAndAddChangeTemplate(r); var invokeMemento = this.genUpdateDirectiveOrElement(r);
var addToChanges = this.genAddToChanges(r); var addToChanges = this.genAddToChanges(r);
var lastInDirective = this.genLastInDirective(r); var lastInDirective = this.genNotifyOnChanges(r);
return pipeCheckTemplate(r.selfIndex - 1, context, bpc, pipe, r.name, oldValue, newValue, change, return pipeCheckTemplate(r.selfIndex - 1, context, bpc, pipe, r.name, oldValue, newValue, change,
invokeMemento, addToChanges, lastInDirective); invokeMemento, addToChanges, lastInDirective);
@ -354,9 +377,9 @@ export class ChangeDetectorJITGenerator {
var change = this.changeNames[r.selfIndex]; var change = this.changeNames[r.selfIndex];
var assignment = this.genUpdateCurrentValue(r); var assignment = this.genUpdateCurrentValue(r);
var invokeMemento = this.getInvokeMementoAndAddChangeTemplate(r); var invokeMemento = this.genUpdateDirectiveOrElement(r);
var addToChanges = this.genAddToChanges(r); var addToChanges = this.genAddToChanges(r);
var lastInDirective = this.genLastInDirective(r); var lastInDirective = this.genNotifyOnChanges(r);
var check = referenceCheckTemplate(r.selfIndex - 1, assignment, oldValue, newValue, change, var check = referenceCheckTemplate(r.selfIndex - 1, assignment, oldValue, newValue, change,
invokeMemento, addToChanges, lastInDirective); invokeMemento, addToChanges, lastInDirective);
@ -426,10 +449,18 @@ export class ChangeDetectorJITGenerator {
return JSON.stringify(value); return JSON.stringify(value);
} }
getInvokeMementoAndAddChangeTemplate(r:ProtoRecord):string { genUpdateDirectiveOrElement(r:ProtoRecord):string {
if (! r.lastInBinding) return "";
var newValue = this.localNames[r.selfIndex]; var newValue = this.localNames[r.selfIndex];
var oldValue = this.fieldNames[r.selfIndex]; var oldValue = this.fieldNames[r.selfIndex];
return r.lastInBinding ? invokeBindingMemento(oldValue, newValue) : "";
if (isPresent(r.directiveMemento)) {
var directiveProperty = `${this.getDirective(r.directiveMemento)}.${r.bindingMemento.propertyName}`;
return updateDirectiveTemplate(oldValue, newValue, directiveProperty);
} else {
return updateElementTemplate(oldValue, newValue);
}
} }
genAddToChanges(r:ProtoRecord):string { genAddToChanges(r:ProtoRecord):string {
@ -439,9 +470,13 @@ export class ChangeDetectorJITGenerator {
return callOnChange ? addToChangesTemplate(oldValue, newValue) : ""; return callOnChange ? addToChangesTemplate(oldValue, newValue) : "";
} }
genLastInDirective(r:ProtoRecord):string{ genNotifyOnChanges(r:ProtoRecord):string{
var callOnChange = r.directiveMemento && r.directiveMemento.callOnChange; var callOnChange = r.directiveMemento && r.directiveMemento.callOnChange;
return r.lastInDirective && callOnChange ? lastInDirectiveTemplate(r.selfIndex - 1) : ''; if (r.lastInDirective && callOnChange) {
return notifyOnChangesTemplate(this.getDirective(r.directiveMemento));
} else {
return "";
}
} }
genArgs(r:ProtoRecord):string { genArgs(r:ProtoRecord):string {

View File

@ -34,6 +34,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
prevContexts:List; prevContexts:List;
protos:List<ProtoRecord>; protos:List<ProtoRecord>;
directives:any;
directiveMementos:List; directiveMementos:List;
changeControlStrategy:string; changeControlStrategy:string;
@ -53,16 +54,18 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
ListWrapper.fill(this.prevContexts, uninitialized); ListWrapper.fill(this.prevContexts, uninitialized);
ListWrapper.fill(this.changes, false); ListWrapper.fill(this.changes, false);
this.locals = null; this.locals = null;
this.directives = null;
this.protos = protoRecords; this.protos = protoRecords;
this.directiveMementos = directiveMementos; this.directiveMementos = directiveMementos;
this.changeControlStrategy = changeControlStrategy; this.changeControlStrategy = changeControlStrategy;
} }
hydrate(context:any, locals:any) { hydrate(context:any, locals:any, directives:any) {
this.mode = ChangeDetectionUtil.changeDetectionMode(this.changeControlStrategy); this.mode = ChangeDetectionUtil.changeDetectionMode(this.changeControlStrategy);
this.values[0] = context; this.values[0] = context;
this.locals = locals; this.locals = locals;
this.directives = directives;
} }
dehydrate() { dehydrate() {
@ -90,29 +93,18 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var protos:List<ProtoRecord> = this.protos; var protos:List<ProtoRecord> = this.protos;
var changes = null; var changes = null;
var currentDirectiveMemento = null;
for (var i = 0; i < protos.length; ++i) { for (var i = 0; i < protos.length; ++i) {
var proto:ProtoRecord = protos[i]; var proto:ProtoRecord = protos[i];
if (isBlank(currentDirectiveMemento)) {
currentDirectiveMemento = proto.directiveMemento;
}
var change = this._check(proto); var change = this._check(proto);
if (isPresent(change)) { if (isPresent(change)) {
if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, change); if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, change);
this.dispatcher.invokeMementoFor(proto.bindingMemento, change.currentValue); this._updateDirectiveOrElement(change, proto.directiveMemento, proto.bindingMemento);
changes = this._addChange(proto.directiveMemento, proto.bindingMemento, change, changes);
if (isPresent(currentDirectiveMemento) && currentDirectiveMemento.callOnChange) {
changes = ChangeDetectionUtil.addChange(changes, proto.bindingMemento, change);
}
} }
if (proto.lastInDirective) { if (proto.lastInDirective && isPresent(changes)) {
if (isPresent(changes)) { this._directive(proto.directiveMemento).onChange(changes);
this.dispatcher.onChange(currentDirectiveMemento, changes);
}
currentDirectiveMemento = null;
changes = null; changes = null;
} }
} }
@ -123,11 +115,31 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
for (var i = mementos.length - 1; i >= 0; --i) { for (var i = mementos.length - 1; i >= 0; --i) {
var memento = mementos[i]; var memento = mementos[i];
if (memento.callOnAllChangesDone) { if (memento.callOnAllChangesDone) {
this.dispatcher.onAllChangesDone(memento); this._directive(memento).onAllChangesDone();
} }
} }
} }
_updateDirectiveOrElement(change, directiveMemento, bindingMemento) {
if (isBlank(directiveMemento)) {
this.dispatcher.invokeMementoFor(bindingMemento, change.currentValue);
} else {
bindingMemento.setter(this._directive(directiveMemento), change.currentValue);
}
}
_addChange(directiveMemento, bindingMemento, change, changes) {
if (isPresent(directiveMemento) && directiveMemento.callOnChange) {
return ChangeDetectionUtil.addChange(changes, bindingMemento, change);
} else {
return changes;
}
}
_directive(memento) {
return memento.directive(this.directives);
}
_check(proto:ProtoRecord) { _check(proto:ProtoRecord) {
try { try {
if (proto.mode === RECORD_TYPE_PIPE || proto.mode === RECORD_TYPE_BINDING_PIPE) { if (proto.mode === RECORD_TYPE_PIPE || proto.mode === RECORD_TYPE_BINDING_PIPE) {

View File

@ -1,16 +1,17 @@
import {List} from 'angular2/src/facade/collection'; import {List} from 'angular2/src/facade/collection';
import {Locals} from './parser/locals'; import {Locals} from './parser/locals';
import {AST} from './parser/ast'; import {AST} from './parser/ast';
import {DEFAULT} from './constants';
export class ProtoChangeDetector { export class ProtoChangeDetector {
addAst(ast:AST, bindingMemento:any, directiveMemento:any = null){} addAst(ast:AST, bindingMemento:any, directiveMemento:any = null){}
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List, directiveMemento:List):ChangeDetector{ instantiate(dispatcher:any, bindingRecords:List, variableBindings:List, directiveMementos:List):ChangeDetector{
return null; return null;
} }
} }
export class ChangeDetection { export class ChangeDetection {
createProtoChangeDetector(name:string, changeControlStrategy:string):ProtoChangeDetector{ createProtoChangeDetector(name:string, changeControlStrategy:string=DEFAULT):ProtoChangeDetector{
return null; return null;
} }
} }
@ -35,7 +36,7 @@ export class ChangeRecord {
} }
export class ChangeDispatcher { export class ChangeDispatcher {
onRecordChange(directiveMemento, records:List<ChangeRecord>) {} invokeMementoFor(memento:any, value) {}
} }
export class ChangeDetector { export class ChangeDetector {
@ -45,7 +46,7 @@ export class ChangeDetector {
addChild(cd:ChangeDetector) {} addChild(cd:ChangeDetector) {}
removeChild(cd:ChangeDetector) {} removeChild(cd:ChangeDetector) {}
remove() {} remove() {}
hydrate(context:any, locals:Locals) {} hydrate(context:any, locals:Locals, directives:any) {}
dehydrate() {} dehydrate() {}
markPathToRootAsCheckOnce() {} markPathToRootAsCheckOnce() {}

View File

@ -67,10 +67,14 @@ export class View {
return isPresent(this.context); return isPresent(this.context);
} }
_hydrateContext(newContext, locals) { _setContextAndLocals(newContext, locals) {
this.context = newContext; this.context = newContext;
this.locals.parent = locals; this.locals.parent = locals;
this.changeDetector.hydrate(this.context, this.locals); this.changeDetector.hydrate(this.context, this.locals, this.elementInjectors);
}
_hydrateChangeDetector() {
this.changeDetector.hydrate(this.context, this.locals, this.elementInjectors);
} }
_dehydrateContext() { _dehydrateContext() {
@ -117,7 +121,8 @@ export class View {
if (this.hydrated()) throw new BaseException('The view is already hydrated.'); if (this.hydrated()) throw new BaseException('The view is already hydrated.');
this.render = renderComponentViewRefs[renderComponentIndex++]; this.render = renderComponentViewRefs[renderComponentIndex++];
this._hydrateContext(context, locals);
this._setContextAndLocals(context, locals);
// viewContainers // viewContainers
for (var i = 0; i < this.viewContainers.length; i++) { for (var i = 0; i < this.viewContainers.length; i++) {
@ -173,6 +178,7 @@ export class View {
componentChildViewIndex++; componentChildViewIndex++;
} }
} }
this._hydrateChangeDetector();
this.proto.renderer.setEventDispatcher(this.render, this); this.proto.renderer.setEventDispatcher(this.render, this);
return renderComponentIndex; return renderComponentIndex;
} }
@ -222,22 +228,9 @@ export class View {
this.dispatchEvent(binderIndex, eventName, locals); this.dispatchEvent(binderIndex, eventName, locals);
} }
onAllChangesDone(directiveMemento:DirectiveMemento) {
var dir = directiveMemento.directive(this.elementInjectors);
dir.onAllChangesDone();
}
onChange(directiveMemento:DirectiveMemento, changes) {
var dir = directiveMemento.directive(this.elementInjectors);
dir.onChange(changes);
}
// dispatch to element injector or text nodes based on context // dispatch to element injector or text nodes based on context
invokeMementoFor(memento:any, currentValue:any) { invokeMementoFor(memento:any, currentValue:any) {
if (memento instanceof DirectiveBindingMemento) { if (memento instanceof ElementBindingMemento) {
var directiveMemento:DirectiveBindingMemento = memento;
directiveMemento.invoke(currentValue, this.elementInjectors);
} else if (memento instanceof ElementBindingMemento) {
var elementMemento:ElementBindingMemento = memento; var elementMemento:ElementBindingMemento = memento;
this.proto.renderer.setElementProperty( this.proto.renderer.setElementProperty(
this.render, elementMemento.elementIndex, elementMemento.propertyName, currentValue this.render, elementMemento.elementIndex, elementMemento.propertyName, currentValue
@ -461,7 +454,7 @@ export class DirectiveBindingMemento {
_elementInjectorIndex:int; _elementInjectorIndex:int;
_directiveIndex:int; _directiveIndex:int;
propertyName:string; propertyName:string;
_setter:SetterFn; setter:SetterFn;
constructor( constructor(
elementInjectorIndex:number, elementInjectorIndex:number,
directiveIndex:number, directiveIndex:number,
@ -470,13 +463,13 @@ export class DirectiveBindingMemento {
this._elementInjectorIndex = elementInjectorIndex; this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex; this._directiveIndex = directiveIndex;
this.propertyName = propertyName; this.propertyName = propertyName;
this._setter = setter; this.setter = setter;
} }
invoke(currentValue:any, elementInjectors:List<ElementInjector>) { invoke(currentValue:any, elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex]; var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
var directive = elementInjector.getDirectiveAtIndex(this._directiveIndex); var directive = elementInjector.getDirectiveAtIndex(this._directiveIndex);
this._setter(directive, currentValue); this.setter(directive, currentValue);
} }
} }
@ -486,6 +479,10 @@ class DirectiveMemento {
callOnAllChangesDone:boolean; callOnAllChangesDone:boolean;
callOnChange:boolean; callOnChange:boolean;
get name() {
return `${this._elementInjectorIndex}_${this._directiveIndex}`;
}
constructor(elementInjectorIndex:number, directiveIndex:number, callOnAllChangesDone:boolean, constructor(elementInjectorIndex:number, directiveIndex:number, callOnAllChangesDone:boolean,
callOnChange:boolean) { callOnChange:boolean) {
this._elementInjectorIndex = elementInjectorIndex; this._elementInjectorIndex = elementInjectorIndex;

View File

@ -42,9 +42,10 @@ export function main() {
var dispatcher = new TestDispatcher(); var dispatcher = new TestDispatcher();
var variableBindings = convertLocalsToVariableBindings(locals); var variableBindings = convertLocalsToVariableBindings(locals);
var records = [new BindingRecord(ast(exp), memo, new FakeDirectiveMemento(memo, false))];
var records = [new BindingRecord(ast(exp), memo, null)];
var cd = pcd.instantiate(dispatcher, records, variableBindings, []); var cd = pcd.instantiate(dispatcher, records, variableBindings, []);
cd.hydrate(context, locals); cd.hydrate(context, locals, null);
return {"changeDetector" : cd, "dispatcher" : dispatcher}; return {"changeDetector" : cd, "dispatcher" : dispatcher};
} }
@ -55,8 +56,9 @@ export function main() {
return res["dispatcher"].log; return res["dispatcher"].log;
} }
function instantiate(protoChangeDetector, dispatcher, bindings) { function instantiate(protoChangeDetector, dispatcher, bindings, directiveMementos = null) {
return protoChangeDetector.instantiate(dispatcher, bindings, null, []); if (isBlank(directiveMementos)) directiveMementos = [];
return protoChangeDetector.instantiate(dispatcher, bindings, null, directiveMementos);
} }
describe(`${name} change detection`, () => { describe(`${name} change detection`, () => {
@ -68,6 +70,7 @@ export function main() {
it('should do simple watching', () => { it('should do simple watching', () => {
var person = new Person("misko"); var person = new Person("misko");
var c = createChangeDetector('name', 'name', person); var c = createChangeDetector('name', 'name', person);
var cd = c["changeDetector"]; var cd = c["changeDetector"];
var dispatcher = c["dispatcher"]; var dispatcher = c["dispatcher"];
@ -202,7 +205,7 @@ export function main() {
var ast = parser.parseInterpolation("B{{a}}A", "location"); var ast = parser.parseInterpolation("B{{a}}A", "location");
var cd = instantiate(pcd, dispatcher, [new BindingRecord(ast, "memo", null)]); var cd = instantiate(pcd, dispatcher, [new BindingRecord(ast, "memo", null)]);
cd.hydrate(new TestData("value"), null); cd.hydrate(new TestData("value"), null, null);
cd.detectChanges(); cd.detectChanges();
@ -238,98 +241,133 @@ export function main() {
}); });
}); });
describe("onChange", () => { describe("updatingDirectives", () => {
var dirMemento1 = new FakeDirectiveMemento(1, false, true); var dirMemento1 = new FakeDirectiveMemento(0, true, true);
var dirMemento2 = new FakeDirectiveMemento(2, false, true); var dirMemento2 = new FakeDirectiveMemento(1, true, true);
var dirMementoNoOnChange = new FakeDirectiveMemento(3, false, false); var dirMementoNoCallbacks = new FakeDirectiveMemento(0, false, false);
var memo1 = new FakeBindingMemento("memo1");
var memo2 = new FakeBindingMemento("memo2");
it("should notify the dispatcher when a group of records changes", () => { var updateA = new FakeBindingMemento((o, v) => o.a = v, "a");
var pcd = createProtoChangeDetector(); var updateB = new FakeBindingMemento((o, v) => o.b = v, "b");
var cd = instantiate(pcd, dispatcher, [ var directive1;
new BindingRecord(ast("1 + 2"), memo1, dirMemento1), var directive2;
new BindingRecord(ast("10 + 20"), memo2, dirMemento1),
new BindingRecord(ast("100 + 200"), memo1, dirMemento2)
]);
cd.detectChanges(); beforeEach(() => {
directive1 = new TestDirective();
expect(dispatcher.loggedOnChange).toEqual([{'memo1': 3, 'memo2': 30}, {'memo1': 300}]); directive2 = new TestDirective();
}); });
it("should not notify the dispatcher when callOnChange is false", () => { it("should happen directly, without invoking the dispatcher", () => {
var pcd = createProtoChangeDetector(); var pcd = createProtoChangeDetector();
var cd = instantiate(pcd, dispatcher, [ var cd = instantiate(pcd, dispatcher, [new BindingRecord(ast("42"), updateA, dirMemento1)],
new BindingRecord(ast("1"), memo1, dirMemento1), [dirMemento1]);
new BindingRecord(ast("2"), memo1, dirMementoNoOnChange),
new BindingRecord(ast("3"), memo1, dirMemento2) cd.hydrate(null, null, [directive1])
]);
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.loggedOnChange).toEqual([{'memo1': 1}, {'memo1': 3}]); expect(dispatcher.loggedValues).toEqual([]);
expect(directive1.a).toEqual(42);
});
describe("onChange", () => {
it("should notify the directive when a group of records changes", () => {
var pcd = createProtoChangeDetector();
var cd = instantiate(pcd, dispatcher, [
new BindingRecord(ast("1"), updateA, dirMemento1),
new BindingRecord(ast("2"), updateB, dirMemento1),
new BindingRecord(ast("3"), updateA, dirMemento2)
], [dirMemento1, dirMemento2]);
cd.hydrate(null, null, [directive1, directive2])
cd.detectChanges();
expect(directive1.changes).toEqual({'a': 1, 'b': 2});
expect(directive2.changes).toEqual({'a': 3});
});
it("should not call onChange when callOnChange is false", () => {
var pcd = createProtoChangeDetector();
var cd = instantiate(pcd, dispatcher, [
new BindingRecord(ast("1"), updateA, dirMementoNoCallbacks)
], [dirMementoNoCallbacks]);
cd.hydrate(null, null, [directive1])
cd.detectChanges();
expect(directive1.changes).toEqual(null);
}); });
}); });
describe("onAllChangesDone", () => { describe("onAllChangesDone", () => {
it("should notify the dispatcher about processing all the children", () => { it("should be called after processing all the children", () => {
var memento1 = new FakeDirectiveMemento(1, false);
var memento2 = new FakeDirectiveMemento(2, true);
var pcd = createProtoChangeDetector(); var pcd = createProtoChangeDetector();
var cd = pcd.instantiate(dispatcher, [], null, [memento1, memento2]);
cd.hydrate(null, null); var cd = instantiate(pcd, dispatcher, [], [dirMemento1, dirMemento2]);
cd.hydrate(null, null, [directive1, directive2]);
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.loggedValues).toEqual([ expect(directive1.onChangesDoneCalled).toBe(true);
["onAllChangesDone", memento2] expect(directive2.onChangesDoneCalled).toBe(true);
]);
}); });
it("should notify in reverse order so the child is always notified before the parent", () => {
var memento1 = new FakeDirectiveMemento(1, true);
var memento2 = new FakeDirectiveMemento(2, true);
it("should not be called when onAllChangesDone is false", () => {
var pcd = createProtoChangeDetector(); var pcd = createProtoChangeDetector();
var cd = pcd.instantiate(dispatcher, [], null, [memento1, memento2]);
cd.hydrate(null, null); var cd = instantiate(pcd, dispatcher, [
new BindingRecord(ast("1"), updateA, dirMementoNoCallbacks)
], [dirMementoNoCallbacks]);
cd.hydrate(null, null, [directive1])
cd.detectChanges(); cd.detectChanges();
expect(dispatcher.loggedValues).toEqual([ expect(directive1.onChangesDoneCalled).toEqual(false);
["onAllChangesDone", memento2],
["onAllChangesDone", memento1]
]);
}); });
it("should notify the dispatcher before processing shadow dom children", () => { it("should be called in reverse order so the child is always notified before the parent", () => {
var memento = new FakeDirectiveMemento(1, true); var pcd = createProtoChangeDetector();
var cd = instantiate(pcd, dispatcher, [], [dirMemento1, dirMemento2]);
var onChangesDoneCalls = [];
var td1;
td1 = new TestDirective(() => ListWrapper.push(onChangesDoneCalls, td1));
var td2;
td2 = new TestDirective(() => ListWrapper.push(onChangesDoneCalls, td2));
cd.hydrate(null, null, [td1, td2]);
cd.detectChanges();
expect(onChangesDoneCalls).toEqual([td2, td1]);
});
it("should be called before processing shadow dom children", () => {
var pcd = createProtoChangeDetector(); var pcd = createProtoChangeDetector();
var shadowDomChildPCD = createProtoChangeDetector(); var shadowDomChildPCD = createProtoChangeDetector();
var parent = pcd.instantiate(dispatcher, [], null, [memento]); var parent = pcd.instantiate(dispatcher, [], null, [dirMemento1]);
var child = shadowDomChildPCD.instantiate(dispatcher, [ var child = shadowDomChildPCD.instantiate(dispatcher, [
new BindingRecord(ast("1"), "a", memento)], null, []); new BindingRecord(ast("1"), updateA, dirMemento1)], null, [dirMemento1]);
parent.addShadowDomChild(child); parent.addShadowDomChild(child);
parent.hydrate(null, null); var directiveInShadowDOm = new TestDirective();
child.hydrate(null, null); var parentDirective = new TestDirective(() => {
expect(directiveInShadowDOm.a).toBe(null);
});
parent.hydrate(null, null, [parentDirective]);
child.hydrate(null, null, [directiveInShadowDOm]);
parent.detectChanges(); parent.detectChanges();
});
// loggedValues contains everything that the dispatcher received
// the first value is the directive memento passed into onAllChangesDone
expect(dispatcher.loggedValues).toEqual([
["onAllChangesDone", memento],
1
]);
}); });
}); });
}); });
@ -343,7 +381,7 @@ export function main() {
var cd = instantiate(pcd, dispatcher, [ var cd = instantiate(pcd, dispatcher, [
new BindingRecord(ast("a"), "a", 1) new BindingRecord(ast("a"), "a", 1)
]); ]);
cd.hydrate(new TestData('value'), null); cd.hydrate(new TestData('value'), null, null);
expect(() => { expect(() => {
cd.checkNoChanges(); cd.checkNoChanges();
@ -448,7 +486,7 @@ export function main() {
expect(cd.mode).toEqual(null); expect(cd.mode).toEqual(null);
cd.hydrate(null, null); cd.hydrate(null, null, null);
expect(cd.mode).toEqual(CHECK_ALWAYS); expect(cd.mode).toEqual(CHECK_ALWAYS);
}); });
@ -456,7 +494,7 @@ export function main() {
it("should set the mode to CHECK_ONCE when the push change detection is used", () => { it("should set the mode to CHECK_ONCE when the push change detection is used", () => {
var proto = createProtoChangeDetector(null, ON_PUSH); var proto = createProtoChangeDetector(null, ON_PUSH);
var cd = proto.instantiate(null, [], [], []); var cd = proto.instantiate(null, [], [], []);
cd.hydrate(null, null); cd.hydrate(null, null, null);
expect(cd.mode).toEqual(CHECK_ONCE); expect(cd.mode).toEqual(CHECK_ONCE);
}); });
@ -536,13 +574,13 @@ export function main() {
var c = createChangeDetector("memo", "name"); var c = createChangeDetector("memo", "name");
var cd = c["changeDetector"]; var cd = c["changeDetector"];
cd.hydrate("some context", null); cd.hydrate("some context", null, null);
expect(cd.hydrated()).toBe(true); expect(cd.hydrated()).toBe(true);
cd.dehydrate(); cd.dehydrate();
expect(cd.hydrated()).toBe(false); expect(cd.hydrated()).toBe(false);
cd.hydrate("other context", null); cd.hydrate("other context", null, null);
expect(cd.hydrated()).toBe(true); expect(cd.hydrated()).toBe(true);
}); });
@ -726,10 +764,33 @@ class FakePipeRegistry extends PipeRegistry {
} }
} }
class TestRecord { class TestDirective {
a; a;
b; b;
c; changes;
onChangesDoneCalled;
onChangesDoneSpy;
constructor(onChangesDoneSpy = null) {
this.onChangesDoneCalled = false;
this.onChangesDoneSpy = onChangesDoneSpy;
this.a = null;
this.b = null;
this.changes = null;
}
onChange(changes) {
var r = {};
StringMapWrapper.forEach(changes, (c, key) => r[key] = c.currentValue);
this.changes = r;
}
onAllChangesDone() {
this.onChangesDoneCalled = true;
if(isPresent(this.onChangesDoneSpy)) {
this.onChangesDoneSpy();
}
}
} }
class Person { class Person {
@ -776,21 +837,31 @@ class TestData {
} }
class FakeDirectiveMemento { class FakeDirectiveMemento {
value:any;
callOnAllChangesDone:boolean; callOnAllChangesDone:boolean;
callOnChange:boolean; callOnChange:boolean;
directiveIndex:number;
constructor(value, callOnAllChangesDone:boolean = false, callOnChange:boolean = false) { constructor(directiveIndex:number = 0, callOnAllChangesDone:boolean = false, callOnChange:boolean = false) {
this.value = value; this.directiveIndex = directiveIndex;
this.callOnAllChangesDone = callOnAllChangesDone; this.callOnAllChangesDone = callOnAllChangesDone;
this.callOnChange = callOnChange; this.callOnChange = callOnChange;
} }
get name() {
return this.directiveIndex;
}
directive(directives) {
return directives[this.directiveIndex];
}
} }
class FakeBindingMemento { class FakeBindingMemento {
setter:Function;
propertyName:string; propertyName:string;
constructor(propertyName:string) { constructor(setter:Function, propertyName:string) {
this.setter = setter;
this.propertyName = propertyName; this.propertyName = propertyName;
} }
} }
@ -798,7 +869,6 @@ class FakeBindingMemento {
class TestDispatcher extends ChangeDispatcher { class TestDispatcher extends ChangeDispatcher {
log:List; log:List;
loggedValues:List; loggedValues:List;
loggedOnChange:List;
constructor() { constructor() {
super(); super();
@ -808,17 +878,6 @@ class TestDispatcher extends ChangeDispatcher {
clear() { clear() {
this.log = ListWrapper.create(); this.log = ListWrapper.create();
this.loggedValues = ListWrapper.create(); this.loggedValues = ListWrapper.create();
this.loggedOnChange = ListWrapper.create();
}
onChange(directiveMemento, changes) {
var r = {};
StringMapWrapper.forEach(changes, (c, key) => r[key] = c.currentValue);
ListWrapper.push(this.loggedOnChange, r);
}
onAllChangesDone(directiveMemento) {
ListWrapper.push(this.loggedValues, ["onAllChangesDone", directiveMemento]);
} }
invokeMementoFor(memento, value) { invokeMementoFor(memento, value) {

View File

@ -17,6 +17,17 @@ describe('ng2 tree benchmark', function () {
}).then(done, done.fail); }).then(done, done.fail);
}); });
it('should log the ng stats (update)', function(done) {
perfUtil.runClickBenchmark({
url: URL,
buttons: ['#ng2CreateDom'],
id: 'ng2.tree.update',
params: [{
name: 'depth', value: 9, scale: 'log2'
}]
}).then(done, done.fail);
});
it('should log the baseline stats', function(done) { it('should log the baseline stats', function(done) {
perfUtil.runClickBenchmark({ perfUtil.runClickBenchmark({
url: URL, url: URL,
@ -28,4 +39,15 @@ describe('ng2 tree benchmark', function () {
}).then(done, done.fail); }).then(done, done.fail);
}); });
it('should log the baseline stats (update)', function(done) {
perfUtil.runClickBenchmark({
url: URL,
buttons: ['#baselineCreateDom'],
id: 'baseline.tree',
params: [{
name: 'depth', value: 9, scale: 'log2'
}]
}).then(done, done.fail);
});
}); });

View File

@ -185,27 +185,29 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations, objec
var dispatcher = new DummyDispatcher(); var dispatcher = new DummyDispatcher();
var parser = new Parser(new Lexer()); var parser = new Parser(new Lexer());
var parentProto = changeDetection.createProtoChangeDetector('parent', null); var parentProto = changeDetection.createProtoChangeDetector('parent');
var parentCd = parentProto.instantiate(dispatcher, [], [], []); var parentCd = parentProto.instantiate(dispatcher, [], [], []);
var targetObj = new Obj(); var targetObj = new Obj();
var proto = changeDetection.createProtoChangeDetector("proto", null); var proto = changeDetection.createProtoChangeDetector("proto");
var directiveMemento = new FakeDirectiveMemento("target", targetObj);
var bindingRecords = [ var bindingRecords = [
new BindingRecord(parser.parseBinding('field0', null), new FakeBindingMemento(targetObj, reflector.setter("field0")), null), new BindingRecord(parser.parseBinding('field0', null), new FakeBindingMemento(reflector.setter("field0"), "field0"), directiveMemento),
new BindingRecord(parser.parseBinding('field1', null), new FakeBindingMemento(targetObj, reflector.setter("field1")), null), new BindingRecord(parser.parseBinding('field1', null), new FakeBindingMemento(reflector.setter("field1"), "field1"), directiveMemento),
new BindingRecord(parser.parseBinding('field2', null), new FakeBindingMemento(targetObj, reflector.setter("field2")), null), new BindingRecord(parser.parseBinding('field2', null), new FakeBindingMemento(reflector.setter("field2"), "field2"), directiveMemento),
new BindingRecord(parser.parseBinding('field3', null), new FakeBindingMemento(targetObj, reflector.setter("field3")), null), new BindingRecord(parser.parseBinding('field3', null), new FakeBindingMemento(reflector.setter("field3"), "field3"), directiveMemento),
new BindingRecord(parser.parseBinding('field4', null), new FakeBindingMemento(targetObj, reflector.setter("field4")), null), new BindingRecord(parser.parseBinding('field4', null), new FakeBindingMemento(reflector.setter("field4"), "field4"), directiveMemento),
new BindingRecord(parser.parseBinding('field5', null), new FakeBindingMemento(targetObj, reflector.setter("field5")), null), new BindingRecord(parser.parseBinding('field5', null), new FakeBindingMemento(reflector.setter("field5"), "field5"), directiveMemento),
new BindingRecord(parser.parseBinding('field6', null), new FakeBindingMemento(targetObj, reflector.setter("field6")), null), new BindingRecord(parser.parseBinding('field6', null), new FakeBindingMemento(reflector.setter("field6"), "field6"), directiveMemento),
new BindingRecord(parser.parseBinding('field7', null), new FakeBindingMemento(targetObj, reflector.setter("field7")), null), new BindingRecord(parser.parseBinding('field7', null), new FakeBindingMemento(reflector.setter("field7"), "field7"), directiveMemento),
new BindingRecord(parser.parseBinding('field8', null), new FakeBindingMemento(targetObj, reflector.setter("field8")), null), new BindingRecord(parser.parseBinding('field8', null), new FakeBindingMemento(reflector.setter("field8"), "field8"), directiveMemento),
new BindingRecord(parser.parseBinding('field9', null), new FakeBindingMemento(targetObj, reflector.setter("field9")), null) new BindingRecord(parser.parseBinding('field9', null), new FakeBindingMemento(reflector.setter("field9"), "field9"), directiveMemento)
]; ];
for (var i = 0; i < iterations; ++i) { for (var i = 0; i < iterations; ++i) {
var cd = proto.instantiate(dispatcher, bindingRecords, [], []); var cd = proto.instantiate(dispatcher, bindingRecords, [], [directiveMemento]);
cd.hydrate(object, null); cd.hydrate(object, null, null);
parentCd.addChild(cd); parentCd.addChild(cd);
} }
return parentCd; return parentCd;
@ -298,17 +300,34 @@ export function main () {
class FakeBindingMemento { class FakeBindingMemento {
setter:Function; setter:Function;
targetObj:Obj; propertyName:string;
constructor(targetObj:Obj, setter:Function) { constructor(setter:Function, propertyName:string) {
this.targetObj = targetObj;
this.setter = setter; this.setter = setter;
this.propertyName = propertyName;
}
}
class FakeDirectiveMemento {
targetObj:Obj;
name:string;
callOnChange:boolean;
callOnAllChangesDone:boolean;
constructor(name, targetObj) {
this.targetObj = targetObj;
this.name = name;
this.callOnChange = false;
this.callOnAllChangesDone = false;
}
directive(dirs) {
return this.targetObj;
} }
} }
class DummyDispatcher extends ChangeDispatcher { class DummyDispatcher extends ChangeDispatcher {
invokeMementoFor(bindingMemento, newValue) { invokeMementoFor(bindingMemento, newValue) {
var obj = bindingMemento.targetObj; throw "Should not be used";
bindingMemento.setter(obj, newValue);
} }
} }