feat(change_detection): implement hydration/dehydration

This commit is contained in:
vsavkin 2015-02-27 07:56:50 -08:00
parent c1dc3ccf48
commit 21f24d19dd
8 changed files with 159 additions and 38 deletions

View File

@ -30,9 +30,9 @@ import {
* this.dispatcher = dispatcher;
* this.protos = protos;
*
* this.context = null;
* this.address0 = null;
* this.city1 = null;
* this.context = ChangeDetectionUtil.unitialized();
* this.address0 = ChangeDetectionUtil.unitialized();
* this.city1 = ChangeDetectionUtil.unitialized();
* }
* ChangeDetector0.prototype = Object.create(AbstractChangeDetector.prototype);
*
@ -70,10 +70,20 @@ import {
* }
*
*
* ChangeDetector0.prototype.setContext = function(context) {
* ChangeDetector0.prototype.hydrate = function(context) {
* this.context = context;
* }
*
* ChangeDetector0.prototype.dehydrate = function(context) {
* this.context = ChangeDetectionUtil.unitialized();
* this.address0 = ChangeDetectionUtil.unitialized();
* this.city1 = ChangeDetectionUtil.unitialized();
* }
*
* ChangeDetector0.prototype.hydrated = function() {
* return this.context !== ChangeDetectionUtil.unitialized();
* }
*
* return ChangeDetector0;
*
*
@ -119,11 +129,22 @@ ${type}.prototype = Object.create(${ABSTRACT_CHANGE_DETECTOR}.prototype);
`;
}
function setContextTemplate(type:string):string {
function pipeOnDestroyTemplate(pipeNames:List) {
return pipeNames.map((p) => `${p}.onDestroy()`).join("\n");
}
function hydrateTemplate(type:string, fieldsDefinitions:string, pipeOnDestroy:string):string {
return `
${type}.prototype.setContext = function(context) {
${type}.prototype.hydrate = function(context) {
this.context = context;
}
${type}.prototype.dehydrate = function() {
${pipeOnDestroy}
${fieldsDefinitions}
}
${type}.prototype.hydrated = function() {
return this.context !== ${UTIL}.unitialized();
}
`;
}
@ -162,7 +183,10 @@ if (${CHANGES_LOCAL} && ${CHANGES_LOCAL}.length > 0) {
function pipeCheckTemplate(context:string, pipe:string, pipeType:string,
value:string, change:string, addRecord:string, notify:string):string{
return `
if (${pipe} === ${UTIL}.unitialized() || !${pipe}.supports(${context})) {
if (${pipe} === ${UTIL}.unitialized()) {
${pipe} = ${PIPE_REGISTRY_ACCESSOR}.get('${pipeType}', ${context});
} else if (!${pipe}.supports(${context})) {
${pipe}.onDestroy();
${pipe} = ${PIPE_REGISTRY_ACCESSOR}.get('${pipeType}', ${context});
}
@ -281,25 +305,34 @@ export class ChangeDetectorJITGenerator {
}
generate():Function {
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genSetContext());
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genHydrate());
return new Function('AbstractChangeDetector', 'ChangeDetectionUtil', 'ContextWithVariableBindings', 'protos', text)(AbstractChangeDetector, ChangeDetectionUtil, ContextWithVariableBindings, this.records);
}
genConstructor():string {
var fields = [];
fields = fields.concat(this.fieldNames);
this.records.forEach((r) => {
if (r.mode === RECORD_TYPE_PIPE) {
fields.push(this.pipeNames[r.selfIndex]);
}
});
return constructorTemplate(this.typeName, fieldDefinitionsTemplate(fields));
return constructorTemplate(this.typeName, this.genFieldDefinitions());
}
genSetContext():string {
return setContextTemplate(this.typeName);
genHydrate():string {
return hydrateTemplate(this.typeName, this.genFieldDefinitions(),
pipeOnDestroyTemplate(this.getnonNullPipeNames()));
}
genFieldDefinitions() {
var fields = [];
fields = fields.concat(this.fieldNames);
fields = fields.concat(this.getnonNullPipeNames());
return fieldDefinitionsTemplate(fields);
}
getnonNullPipeNames():List<String> {
var pipes = [];
this.records.forEach((r) => {
if (r.mode === RECORD_TYPE_PIPE) {
pipes.push(this.pipeNames[r.selfIndex]);
}
});
return pipes;
}
genDetectChanges():string {

View File

@ -43,15 +43,36 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
this.prevContexts = ListWrapper.createFixedSize(protoRecords.length + 1);
this.changes = ListWrapper.createFixedSize(protoRecords.length + 1);
ListWrapper.fill(this.values, uninitialized);
ListWrapper.fill(this.pipes, null);
ListWrapper.fill(this.prevContexts, uninitialized);
ListWrapper.fill(this.changes, false);
this.protos = protoRecords;
}
setContext(context:any) {
hydrate(context:any) {
this.values[0] = context;
}
dehydrate() {
this._destroyPipes();
ListWrapper.fill(this.values, uninitialized);
ListWrapper.fill(this.changes, false);
ListWrapper.fill(this.pipes, null);
ListWrapper.fill(this.prevContexts, uninitialized);
this.values[0] = context;
}
_destroyPipes() {
for(var i = 0; i < this.pipes.length; ++i) {
if (isPresent(this.pipes[i])) {
this.pipes[i].onDestroy();
}
}
}
hydrated():boolean {
return this.values[0] !== uninitialized;
}
detectChangesInRecords(throwOnChange:boolean) {
@ -184,11 +205,13 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var storedPipe = this._readPipe(proto);
if (isPresent(storedPipe) && storedPipe.supports(context)) {
return storedPipe;
} else {
var pipe = this.pipeRegistry.get(proto.name, context);
this._writePipe(proto, pipe);
return pipe;
}
if (isPresent(storedPipe)) {
storedPipe.onDestroy();
}
var pipe = this.pipeRegistry.get(proto.name, context);
this._writePipe(proto, pipe);
return pipe;
}
_readContext(proto:ProtoRecord) {

View File

@ -55,7 +55,8 @@ export class ChangeDetector {
addChild(cd:ChangeDetector) {}
removeChild(cd:ChangeDetector) {}
remove() {}
setContext(context:any) {}
hydrate(context:any) {}
dehydrate() {}
markPathToRootAsCheckOnce() {}
detectChanges() {}

View File

@ -2,5 +2,6 @@ export var NO_CHANGE = new Object();
export class Pipe {
supports(obj):boolean {return false;}
onDestroy() {}
transform(value:any):any {return null;}
}

View File

@ -97,7 +97,7 @@ export class View {
// TODO(tbosch): if we have a contextWithLocals we actually only need to
// set the contextWithLocals once. Would it be faster to always use a contextWithLocals
// even if we don't have locals and not update the recordRange here?
this.changeDetector.setContext(this.context);
this.changeDetector.hydrate(this.context);
}
_dehydrateContext() {
@ -105,6 +105,7 @@ export class View {
this.contextWithLocals.clearValues();
}
this.context = null;
this.changeDetector.dehydrate();
}
/**

View File

@ -7,7 +7,7 @@ import {Parser} from 'angular2/src/change_detection/parser/parser';
import {Lexer} from 'angular2/src/change_detection/parser/lexer';
import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, ContextWithVariableBindings,
PipeRegistry, NO_CHANGE, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from 'angular2/change_detection';
PipeRegistry, Pipe, NO_CHANGE, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from 'angular2/change_detection';
import {ChangeDetectionUtil} from 'angular2/src/change_detection/change_detection_util';
@ -33,7 +33,7 @@ export function main() {
pcd.addAst(ast(exp), memo, memo);
var dispatcher = new TestDispatcher();
var cd = pcd.instantiate(dispatcher);
cd.setContext(context);
cd.hydrate(context);
return {"changeDetector" : cd, "dispatcher" : dispatcher};
}
@ -183,7 +183,7 @@ export function main() {
var dispatcher = new TestDispatcher();
var cd = pcd.instantiate(dispatcher);
cd.setContext(new TestData("value"));
cd.hydrate(new TestData("value"));
cd.detectChanges();
@ -264,7 +264,7 @@ export function main() {
dispatcher.logValue('InvokeC');
return 'c'
};
cd.setContext(tr);
cd.hydrate(tr);
cd.detectChanges();
@ -280,7 +280,7 @@ export function main() {
var dispatcher = new TestDispatcher();
var cd = pcd.instantiate(dispatcher);
cd.setContext(new TestData('value'));
cd.hydrate(new TestData('value'));
expect(() => {
cd.checkNoChanges();
@ -295,7 +295,7 @@ export function main() {
pcd.addAst(ast('invalidProp', 'someComponent'), "a", 1);
var cd = pcd.instantiate(new TestDispatcher());
cd.setContext(null);
cd.hydrate(null);
try {
cd.detectChanges();
@ -442,6 +442,35 @@ export function main() {
});
});
describe("hydration", () => {
it("should be able to rehydrate a change detector", () => {
var c = createChangeDetector("memo", "name");
var cd = c["changeDetector"];
cd.hydrate("some context");
expect(cd.hydrated()).toBe(true);
cd.dehydrate();
expect(cd.hydrated()).toBe(false);
cd.hydrate("other context");
expect(cd.hydrated()).toBe(true);
});
it("should destroy all active pipes during dehyration", () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var c = createChangeDetector("memo", "name | pipe", new Person('bob'), registry);
var cd = c["changeDetector"];
cd.detectChanges();
cd.dehydrate();
expect(pipe.destroyCalled).toBe(true);
});
});
describe("pipes", () => {
it("should support pipes", () => {
var registry = new FakePipeRegistry('pipe', () => new CountingPipe());
@ -477,6 +506,21 @@ export function main() {
expect(registry.numberOfLookups).toEqual(2);
});
it("should invoke onDestroy on a pipe before switching to another one", () => {
var pipe = new OncePipe();
var registry = new FakePipeRegistry('pipe', () => pipe);
var ctx = new Person("Megatron");
var c = createChangeDetector("memo", "name | pipe", ctx, registry);
var cd = c["changeDetector"];
cd.detectChanges();
ctx.name = "Optimus Prime";
cd.detectChanges();
expect(pipe.destroyCalled).toEqual(true);
});
});
it("should do nothing when returns NO_CHANGE", () => {
@ -502,10 +546,11 @@ export function main() {
});
}
class CountingPipe {
class CountingPipe extends Pipe {
state:number;
constructor() {
super();
this.state = 0;
}
@ -518,23 +563,31 @@ class CountingPipe {
}
}
class OncePipe {
class OncePipe extends Pipe {
called:boolean;
destroyCalled:boolean;
constructor() {
super();
this.called = false;;
this.destroyCalled = false;
}
supports(newValue) {
return !this.called;
}
onDestroy() {
this.destroyCalled = true;
}
transform(value) {
this.called = true;
return value;
}
}
class IdentityPipe {
class IdentityPipe extends Pipe {
state:any;
supports(newValue) {

View File

@ -76,6 +76,15 @@ export function main() {
expect(view.hydrated()).toBe(false);
});
it('should hydrate and dehydrate the change detector', () => {
var ctx = new Object();
view.hydrate(null, null, ctx);
expect(view.changeDetector.hydrated()).toBe(true);
view.dehydrate();
expect(view.changeDetector.hydrated()).toBe(false);
});
it('should use the view pool to reuse views', () => {
var pv = new ProtoView(el('<div id="1"></div>'), new DynamicProtoChangeDetector(null), null);
var fakeView = new FakeView();

View File

@ -129,7 +129,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations) {
obj.setField(j, i);
}
var cd = proto.instantiate(dispatcher);
cd.setContext(obj);
cd.hydrate(obj);
parentCd.addChild(cd);
}
return parentCd;