Skip to content

Commit 8db6215

Browse files
committed
fix(compiler): only call pure pipes if their input changed.
1 parent bab81a9 commit 8db6215

File tree

4 files changed

+78
-9
lines changed

4 files changed

+78
-9
lines changed

modules/angular2/src/compiler/view_compiler/compile_view.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class CompileView implements NameResolver {
124124
}
125125
}
126126

127-
createPipe(name: string): o.Expression {
127+
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression {
128128
var pipeMeta: CompilePipeMetadata = null;
129129
for (var i = this.pipeMetas.length - 1; i >= 0; i--) {
130130
var localPipeMeta = this.pipeMetas[i];
@@ -139,6 +139,7 @@ export class CompileView implements NameResolver {
139139
}
140140
var pipeFieldName = pipeMeta.pure ? `_pipe_${name}` : `_pipe_${name}_${this.pipes.size}`;
141141
var pipeExpr = this.pipes.get(pipeFieldName);
142+
var pipeFieldCacheProp = o.THIS_EXPR.prop(`${pipeFieldName}_cache`);
142143
if (isBlank(pipeExpr)) {
143144
var deps = pipeMeta.type.diDeps.map((diDep) => {
144145
if (diDep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef))) {
@@ -148,6 +149,12 @@ export class CompileView implements NameResolver {
148149
});
149150
this.fields.push(
150151
new o.ClassField(pipeFieldName, o.importType(pipeMeta.type), [o.StmtModifier.Private]));
152+
if (pipeMeta.pure) {
153+
this.fields.push(new o.ClassField(pipeFieldCacheProp.name, null, [o.StmtModifier.Private]));
154+
this.createMethod.addStmt(o.THIS_EXPR.prop(pipeFieldCacheProp.name)
155+
.set(o.importExpr(Identifiers.uninitialized))
156+
.toStmt());
157+
}
151158
this.createMethod.resetDebugInfo(null, null);
152159
this.createMethod.addStmt(o.THIS_EXPR.prop(pipeFieldName)
153160
.set(o.importExpr(pipeMeta.type).instantiate(deps))
@@ -156,7 +163,15 @@ export class CompileView implements NameResolver {
156163
this.pipes.set(pipeFieldName, pipeExpr);
157164
bindPipeDestroyLifecycleCallbacks(pipeMeta, pipeExpr, this);
158165
}
159-
return pipeExpr;
166+
var callPipeExpr: o.Expression = pipeExpr.callMethod('transform', [input, o.literalArr(args)]);
167+
if (pipeMeta.pure) {
168+
callPipeExpr =
169+
o.THIS_EXPR.callMethod(
170+
'checkPurePipe',
171+
[o.literal(this.literalArrayCount++), o.literalArr([input].concat(args))])
172+
.conditional(pipeFieldCacheProp.set(callPipeExpr), pipeFieldCacheProp);
173+
}
174+
return callPipeExpr;
160175
}
161176

162177
getVariable(name: string): o.Expression {

modules/angular2/src/compiler/view_compiler/expression_converter.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {isBlank, isPresent, isArray, CONST_EXPR} from 'angular2/src/facade/lang'
88
var IMPLICIT_RECEIVER = o.variable('#implicit');
99

1010
export interface NameResolver {
11-
createPipe(name: string): o.Expression;
11+
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression;
1212
getVariable(name: string): o.Expression;
1313
createLiteralArray(values: o.Expression[]): o.Expression;
1414
createLiteralMap(values: Array<Array<string | o.Expression>>): o.Expression;
@@ -132,13 +132,12 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
132132
ast.falseExp.visit(this, _Mode.Expression)));
133133
}
134134
visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any {
135-
var pipeInstance = this._nameResolver.createPipe(ast.name);
136135
var input = ast.exp.visit(this, _Mode.Expression);
137136
var args = this.visitAll(ast.args, _Mode.Expression);
137+
var pipeResult = this._nameResolver.callPipe(ast.name, input, args);
138138
this.needsValueUnwrapper = true;
139-
return convertToStatementIfNeeded(
140-
mode, this._valueUnwrapper.callMethod(
141-
'unwrap', [pipeInstance.callMethod('transform', [input, o.literalArr(args)])]));
139+
return convertToStatementIfNeeded(mode,
140+
this._valueUnwrapper.callMethod('unwrap', [pipeResult]));
142141
}
143142
visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any {
144143
return convertToStatementIfNeeded(mode, ast.target.visit(this, _Mode.Expression)

modules/angular2/src/core/linker/view.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,22 +360,34 @@ export abstract class AppView<T> {
360360
this.viewContainerElement = null;
361361
}
362362

363+
checkPurePipe(id: number, newArgs: any[]): boolean {
364+
var prevArgs = this._literalArrayCache[id];
365+
var newPresent = isPresent(newArgs);
366+
var prevPresent = isPresent(prevArgs);
367+
if (newPresent !== prevPresent || (newPresent && !arrayLooseIdentical(prevArgs, newArgs))) {
368+
this._literalArrayCache[id] = newArgs;
369+
return true;
370+
} else {
371+
return false;
372+
}
373+
}
374+
363375
literalArray(id: number, value: any[]): any[] {
364-
var prevValue = this._literalArrayCache[id];
365376
if (isBlank(value)) {
366377
return value;
367378
}
379+
var prevValue = this._literalArrayCache[id];
368380
if (isBlank(prevValue) || !arrayLooseIdentical(prevValue, value)) {
369381
prevValue = this._literalArrayCache[id] = value;
370382
}
371383
return prevValue;
372384
}
373385

374386
literalMap(id: number, value: {[key: string]: any}): {[key: string]: any} {
375-
var prevValue = this._literalMapCache[id];
376387
if (isBlank(value)) {
377388
return value;
378389
}
390+
var prevValue = this._literalMapCache[id];
379391
if (isBlank(prevValue) || !mapLooseIdentical(prevValue, value)) {
380392
prevValue = this._literalMapCache[id] = value;
381393
}

modules/angular2/test/core/linker/change_detection_integration_spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,42 @@ export function main() {
491491

492492
expect(renderLog.log).toEqual(['someProp=Megatron']);
493493
}));
494+
495+
it('should call pure pipes only if the arguments change', fakeAsync(() => {
496+
var ctx = _bindSimpleValue('name | countingPipe', Person);
497+
// change from undefined -> null
498+
ctx.componentInstance.name = null;
499+
ctx.detectChanges(false);
500+
expect(renderLog.loggedValues).toEqual(['null state:0']);
501+
ctx.detectChanges(false);
502+
expect(renderLog.loggedValues).toEqual(['null state:0']);
503+
504+
// change from null -> some value
505+
ctx.componentInstance.name = 'bob';
506+
ctx.detectChanges(false);
507+
expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']);
508+
ctx.detectChanges(false);
509+
expect(renderLog.loggedValues).toEqual(['null state:0', 'bob state:1']);
510+
511+
// change from some value -> some other value
512+
ctx.componentInstance.name = 'bart';
513+
ctx.detectChanges(false);
514+
expect(renderLog.loggedValues)
515+
.toEqual(['null state:0', 'bob state:1', 'bart state:2']);
516+
ctx.detectChanges(false);
517+
expect(renderLog.loggedValues)
518+
.toEqual(['null state:0', 'bob state:1', 'bart state:2']);
519+
520+
}));
521+
522+
it('should call impure pipes on each change detection run', fakeAsync(() => {
523+
var ctx = _bindSimpleValue('name | countingImpurePipe', Person);
524+
ctx.componentInstance.name = 'bob';
525+
ctx.detectChanges(false);
526+
expect(renderLog.loggedValues).toEqual(['bob state:0']);
527+
ctx.detectChanges(false);
528+
expect(renderLog.loggedValues).toEqual(['bob state:0', 'bob state:1']);
529+
}));
494530
});
495531

496532
describe('event expressions', () => {
@@ -1014,6 +1050,7 @@ const ALL_DIRECTIVES = CONST_EXPR([
10141050

10151051
const ALL_PIPES = CONST_EXPR([
10161052
forwardRef(() => CountingPipe),
1053+
forwardRef(() => CountingImpurePipe),
10171054
forwardRef(() => MultiArgPipe),
10181055
forwardRef(() => PipeWithOnDestroy),
10191056
forwardRef(() => IdentityPipe),
@@ -1089,6 +1126,12 @@ class CountingPipe implements PipeTransform {
10891126
transform(value, args = null) { return `${value} state:${this.state ++}`; }
10901127
}
10911128

1129+
@Pipe({name: 'countingImpurePipe', pure: false})
1130+
class CountingImpurePipe implements PipeTransform {
1131+
state: number = 0;
1132+
transform(value, args = null) { return `${value} state:${this.state ++}`; }
1133+
}
1134+
10921135
@Pipe({name: 'pipeWithOnDestroy'})
10931136
class PipeWithOnDestroy implements PipeTransform, OnDestroy {
10941137
constructor(private directiveLog: DirectiveLog) {}

0 commit comments

Comments
 (0)