fix(compiler): properly implement pure pipes and change pipe syntax

Pure pipes as well as arrays and maps are
implemented via proxy functions. This is
faster than the previous implementation
and also generates less code.

BREAKING CHANGE:
- pipes now take a variable number of arguments, and not an array that contains all arguments.
This commit is contained in:
Tobias Bosch
2016-04-22 15:33:32 -07:00
parent d6626309fd
commit 152a117d5c
48 changed files with 698 additions and 283 deletions

View File

@ -5,7 +5,18 @@ import {
ViewUtils,
flattenNestedViewRenderNodes,
interpolate,
checkBinding
checkBinding,
castByValue,
pureProxy1,
pureProxy2,
pureProxy3,
pureProxy4,
pureProxy5,
pureProxy6,
pureProxy7,
pureProxy8,
pureProxy9,
pureProxy10
} from 'angular2/src/core/linker/view_utils';
import {
uninitialized,
@ -59,6 +70,7 @@ var impFlattenNestedViewRenderNodes = flattenNestedViewRenderNodes;
var impDevModeEqual = devModeEqual;
var impInterpolate = interpolate;
var impCheckBinding = checkBinding;
var impCastByValue = castByValue;
export class Identifiers {
static ViewUtils = new CompileIdentifierMetadata({
@ -162,6 +174,31 @@ export class Identifiers {
{name: 'devModeEqual', moduleUrl: CD_MODULE_URL, runtime: impDevModeEqual});
static interpolate = new CompileIdentifierMetadata(
{name: 'interpolate', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impInterpolate});
static castByValue = new CompileIdentifierMetadata(
{name: 'castByValue', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impCastByValue});
static pureProxies = [
null,
new CompileIdentifierMetadata(
{name: 'pureProxy1', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy1}),
new CompileIdentifierMetadata(
{name: 'pureProxy2', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy2}),
new CompileIdentifierMetadata(
{name: 'pureProxy3', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy3}),
new CompileIdentifierMetadata(
{name: 'pureProxy4', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy4}),
new CompileIdentifierMetadata(
{name: 'pureProxy5', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy5}),
new CompileIdentifierMetadata(
{name: 'pureProxy6', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy6}),
new CompileIdentifierMetadata(
{name: 'pureProxy7', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy7}),
new CompileIdentifierMetadata(
{name: 'pureProxy8', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy8}),
new CompileIdentifierMetadata(
{name: 'pureProxy9', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy9}),
new CompileIdentifierMetadata(
{name: 'pureProxy10', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy10}),
];
}
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {

View File

@ -197,6 +197,11 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
var name = expr.name;
if (isPresent(expr.builtin)) {
name = this.getBuiltinMethodName(expr.builtin);
if (isBlank(name)) {
// some builtins just mean to skip the call.
// e.g. `bind` in Dart.
return null;
}
}
ctx.print(`.${name}(`);
this.visitAllExpressions(expr.args, ctx, `,`);

View File

@ -153,6 +153,9 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
case o.BuiltinMethod.SubscribeObservable:
name = 'subscribe';
break;
case o.BuiltinMethod.bind:
name = 'bind';
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}

View File

@ -213,6 +213,9 @@ class _DartEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisito
case o.BuiltinMethod.SubscribeObservable:
name = 'listen';
break;
case o.BuiltinMethod.bind:
name = null;
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}

View File

@ -17,8 +17,7 @@ export class InterpretiveAppViewInstanceFactory implements InstanceFactory {
class _InterpretiveAppView extends AppView<any> implements DynamicInstance {
constructor(args: any[], public props: Map<string, any>, public getters: Map<string, Function>,
public methods: Map<string, Function>) {
super(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9],
args[10]);
super(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
}
createInternal(rootSelector: string | any): AppElement {
var m = this.methods.get('createInternal');

View File

@ -244,7 +244,8 @@ export class WritePropExpr extends Expression {
export enum BuiltinMethod {
ConcatArray,
SubscribeObservable
SubscribeObservable,
bind
}
export class InvokeMethodExpr extends Expression {

View File

@ -187,6 +187,13 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
case o.BuiltinMethod.SubscribeObservable:
result = ObservableWrapper.subscribe(receiver, args[0]);
break;
case o.BuiltinMethod.bind:
if (IS_DART) {
result = receiver;
} else {
result = receiver.bind(args[0]);
}
break;
default:
throw new BaseException(`Unknown builtin method ${expr.builtin}`);
}
@ -331,6 +338,8 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
result = di.props.get(ast.name);
} else if (di.getters.has(ast.name)) {
result = di.getters.get(ast.name)();
} else if (di.methods.has(ast.name)) {
result = di.methods.get(ast.name);
} else {
result = reflector.getter(ast.name)(receiver);
}

View File

@ -287,6 +287,9 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor
case o.BuiltinMethod.SubscribeObservable:
name = 'subscribe';
break;
case o.BuiltinMethod.bind:
name = 'bind';
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}

View File

@ -324,7 +324,6 @@ export class CompileElement extends CompileNode {
private _getDependency(requestingProviderType: ProviderAstType,
dep: CompileDiDependencyMetadata): o.Expression {
var currElement: CompileElement = this;
var currView = currElement.view;
var result = null;
if (dep.isValue) {
result = o.literal(dep.value);
@ -332,14 +331,9 @@ export class CompileElement extends CompileNode {
if (isBlank(result) && !dep.isSkipSelf) {
result = this._getLocalDependency(requestingProviderType, dep);
}
var resultViewPath = [];
// check parent elements
while (isBlank(result) && !currElement.parent.isNull()) {
currElement = currElement.parent;
while (currElement.view !== currView && currView != null) {
currView = currView.declarationElement.view;
resultViewPath.push(currView);
}
result = currElement._getLocalDependency(ProviderAstType.PublicService,
new CompileDiDependencyMetadata({token: dep.token}));
}
@ -350,7 +344,7 @@ export class CompileElement extends CompileNode {
if (isBlank(result)) {
result = o.NULL_EXPR;
}
return getPropertyInView(result, resultViewPath);
return getPropertyInView(result, this.view, currElement.view);
}
}

View File

@ -0,0 +1,76 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from '../output/output_ast';
import {CompileView} from './compile_view';
import {CompilePipeMetadata} from '../compile_metadata';
import {Identifiers, identifierToken} from '../identifiers';
import {injectFromViewParentInjector, createPureProxy, getPropertyInView} from './util';
class _PurePipeProxy {
constructor(public instance: o.ReadPropExpr, public argCount: number) {}
}
export class CompilePipe {
meta: CompilePipeMetadata;
instance: o.ReadPropExpr;
private _purePipeProxies: _PurePipeProxy[] = [];
constructor(public view: CompileView, name: string) {
this.meta = _findPipeMeta(view, name);
this.instance = o.THIS_EXPR.prop(`_pipe_${name}_${view.pipeCount++}`);
}
get pure(): boolean { return this.meta.pure; }
create(): void {
var deps = this.meta.type.diDeps.map((diDep) => {
if (diDep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef))) {
return o.THIS_EXPR.prop('ref');
}
return injectFromViewParentInjector(diDep.token, false);
});
this.view.fields.push(new o.ClassField(this.instance.name, o.importType(this.meta.type),
[o.StmtModifier.Private]));
this.view.createMethod.resetDebugInfo(null, null);
this.view.createMethod.addStmt(o.THIS_EXPR.prop(this.instance.name)
.set(o.importExpr(this.meta.type).instantiate(deps))
.toStmt());
this._purePipeProxies.forEach((purePipeProxy) => {
createPureProxy(
this.instance.prop('transform').callMethod(o.BuiltinMethod.bind, [this.instance]),
purePipeProxy.argCount, purePipeProxy.instance, this.view);
});
}
call(callingView: CompileView, args: o.Expression[]): o.Expression {
if (this.meta.pure) {
var purePipeProxy = new _PurePipeProxy(
o.THIS_EXPR.prop(`${this.instance.name}_${this._purePipeProxies.length}`), args.length);
this._purePipeProxies.push(purePipeProxy);
return getPropertyInView(
o.importExpr(Identifiers.castByValue)
.callFn([purePipeProxy.instance, this.instance.prop('transform')]),
callingView, this.view)
.callFn(args);
} else {
return getPropertyInView(this.instance, callingView, this.view).callMethod('transform', args);
}
}
}
function _findPipeMeta(view: CompileView, name: string): CompilePipeMetadata {
var pipeMeta: CompilePipeMetadata = null;
for (var i = view.pipeMetas.length - 1; i >= 0; i--) {
var localPipeMeta = view.pipeMetas[i];
if (localPipeMeta.name == name) {
pipeMeta = localPipeMeta;
break;
}
}
if (isBlank(pipeMeta)) {
throw new BaseException(
`Illegal state: Could not find pipe ${name} although the parser should have detected this error!`);
}
return pipeMeta;
}

View File

@ -30,14 +30,12 @@ export class CompileQuery {
addValue(value: o.Expression, view: CompileView) {
var currentView = view;
var elPath: CompileElement[] = [];
var viewPath: CompileView[] = [];
while (isPresent(currentView) && currentView !== this.view) {
var parentEl = currentView.declarationElement;
elPath.unshift(parentEl);
currentView = parentEl.view;
viewPath.push(currentView);
}
var queryListForDirtyExpr = getPropertyInView(this.queryList, viewPath);
var queryListForDirtyExpr = getPropertyInView(this.queryList, view, this.view);
var viewValues = this._values;
elPath.forEach((el) => {

View File

@ -1,14 +1,13 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/exceptions';
import {ListWrapper, StringMapWrapper, MapWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
import {Identifiers, identifierToken} from '../identifiers';
import {EventHandlerVars} from './constants';
import {CompileQuery, createQueryList, addQueryToTokenMap} from './compile_query';
import {NameResolver} from './expression_converter';
import {CompileElement, CompileNode} from './compile_element';
import {CompileMethod} from './compile_method';
import {CompilePipe} from './compile_pipe';
import {ViewType} from 'angular2/src/core/linker/view_type';
import {
CompileDirectiveMetadata,
@ -20,17 +19,12 @@ import {
getViewFactoryName,
injectFromViewParentInjector,
createDiTokenExpression,
getPropertyInView
getPropertyInView,
createPureProxy
} from './util';
import {CompilerConfig} from '../config';
import {CompileBinding} from './compile_binding';
import {bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
export class CompilePipe {
constructor() {}
}
export class CompileView implements NameResolver {
public viewType: ViewType;
public viewQueries: CompileTokenMap<CompileQuery[]>;
@ -60,7 +54,8 @@ export class CompileView implements NameResolver {
public subscriptions: o.Expression[] = [];
public componentView: CompileView;
public pipes = new Map<string, o.Expression>();
public purePipes = new Map<string, CompilePipe>();
public pipes: CompilePipe[] = [];
public variables = new Map<string, o.Expression>();
public className: string;
public classType: o.Type;
@ -68,6 +63,7 @@ export class CompileView implements NameResolver {
public literalArrayCount = 0;
public literalMapCount = 0;
public pipeCount = 0;
constructor(public component: CompileDirectiveMetadata, public genConfig: CompilerConfig,
public pipeMetas: CompilePipeMetadata[], public styles: o.Expression,
@ -124,39 +120,17 @@ export class CompileView implements NameResolver {
}
}
createPipe(name: string): o.Expression {
var pipeMeta: CompilePipeMetadata = null;
for (var i = this.pipeMetas.length - 1; i >= 0; i--) {
var localPipeMeta = this.pipeMetas[i];
if (localPipeMeta.name == name) {
pipeMeta = localPipeMeta;
break;
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression {
var compView = this.componentView;
var pipe = compView.purePipes.get(name);
if (isBlank(pipe)) {
pipe = new CompilePipe(compView, name);
if (pipe.pure) {
compView.purePipes.set(name, pipe);
}
compView.pipes.push(pipe);
}
if (isBlank(pipeMeta)) {
throw new BaseException(
`Illegal state: Could not find pipe ${name} although the parser should have detected this error!`);
}
var pipeFieldName = pipeMeta.pure ? `_pipe_${name}` : `_pipe_${name}_${this.pipes.size}`;
var pipeExpr = this.pipes.get(pipeFieldName);
if (isBlank(pipeExpr)) {
var deps = pipeMeta.type.diDeps.map((diDep) => {
if (diDep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef))) {
return o.THIS_EXPR.prop('ref');
}
return injectFromViewParentInjector(diDep.token, false);
});
this.fields.push(
new o.ClassField(pipeFieldName, o.importType(pipeMeta.type), [o.StmtModifier.Private]));
this.createMethod.resetDebugInfo(null, null);
this.createMethod.addStmt(o.THIS_EXPR.prop(pipeFieldName)
.set(o.importExpr(pipeMeta.type).instantiate(deps))
.toStmt());
pipeExpr = o.THIS_EXPR.prop(pipeFieldName);
this.pipes.set(pipeFieldName, pipeExpr);
bindPipeDestroyLifecycleCallbacks(pipeMeta, pipeExpr, this);
}
return pipeExpr;
return pipe.call(this, [input].concat(args));
}
getVariable(name: string): o.Expression {
@ -165,29 +139,49 @@ export class CompileView implements NameResolver {
}
var currView: CompileView = this;
var result = currView.variables.get(name);
var viewPath = [];
while (isBlank(result) && isPresent(currView.declarationElement.view)) {
currView = currView.declarationElement.view;
result = currView.variables.get(name);
viewPath.push(currView);
}
if (isPresent(result)) {
return getPropertyInView(result, viewPath);
return getPropertyInView(result, this, currView);
} else {
return null;
}
}
createLiteralArray(values: o.Expression[]): o.Expression {
return o.THIS_EXPR.callMethod('literalArray',
[o.literal(this.literalArrayCount++), o.literalArr(values)]);
var proxyExpr = o.THIS_EXPR.prop(`_arr_${this.literalArrayCount++}`);
var proxyParams: o.FnParam[] = [];
var proxyReturnEntries: o.Expression[] = [];
for (var i = 0; i < values.length; i++) {
var paramName = `p${i}`;
proxyParams.push(new o.FnParam(paramName));
proxyReturnEntries.push(o.variable(paramName));
}
createPureProxy(o.fn(proxyParams, [new o.ReturnStatement(o.literalArr(proxyReturnEntries))]),
values.length, proxyExpr, this);
return proxyExpr.callFn(values);
}
createLiteralMap(values: Array<Array<string | o.Expression>>): o.Expression {
return o.THIS_EXPR.callMethod('literalMap',
[o.literal(this.literalMapCount++), o.literalMap(values)]);
createLiteralMap(entries: Array<Array<string | o.Expression>>): o.Expression {
var proxyExpr = o.THIS_EXPR.prop(`_map_${this.literalMapCount++}`);
var proxyParams: o.FnParam[] = [];
var proxyReturnEntries: Array<Array<string | o.Expression>> = [];
var values: o.Expression[] = [];
for (var i = 0; i < entries.length; i++) {
var paramName = `p${i}`;
proxyParams.push(new o.FnParam(paramName));
proxyReturnEntries.push([entries[i][0], o.variable(paramName)]);
values.push(<o.Expression>entries[i][1]);
}
createPureProxy(o.fn(proxyParams, [new o.ReturnStatement(o.literalMap(proxyReturnEntries))]),
entries.length, proxyExpr, this);
return proxyExpr.callFn(values);
}
afterNodes() {
this.pipes.forEach((pipe) => pipe.create());
this.viewQueries.values().forEach(
(queries) => queries.forEach((query) => query.afterChildren(this.updateViewQueriesMethod)));
}

View File

@ -8,7 +8,7 @@ import {isBlank, isPresent, isArray, CONST_EXPR} from 'angular2/src/facade/lang'
var IMPLICIT_RECEIVER = o.variable('#implicit');
export interface NameResolver {
createPipe(name: string): o.Expression;
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression;
getVariable(name: string): o.Expression;
createLiteralArray(values: o.Expression[]): o.Expression;
createLiteralMap(values: Array<Array<string | o.Expression>>): o.Expression;
@ -132,13 +132,11 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
ast.falseExp.visit(this, _Mode.Expression)));
}
visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any {
var pipeInstance = this._nameResolver.createPipe(ast.name);
var input = ast.exp.visit(this, _Mode.Expression);
var args = this.visitAll(ast.args, _Mode.Expression);
var value = this._nameResolver.callPipe(ast.name, input, args);
this.needsValueUnwrapper = true;
return convertToStatementIfNeeded(
mode, this._valueUnwrapper.callMethod(
'unwrap', [pipeInstance.callMethod('transform', [input, o.literalArr(args)])]));
return convertToStatementIfNeeded(mode, this._valueUnwrapper.callMethod('unwrap', [value]));
}
visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any {
return convertToStatementIfNeeded(mode, ast.target.visit(this, _Mode.Expression)

View File

@ -78,10 +78,10 @@ export function bindDirectiveDestroyLifecycleCallbacks(directiveMeta: CompileDir
}
}
export function bindPipeDestroyLifecycleCallbacks(
pipeMeta: CompilePipeMetadata, directiveInstance: o.Expression, view: CompileView) {
export function bindPipeDestroyLifecycleCallbacks(pipeMeta: CompilePipeMetadata,
pipeInstance: o.Expression, view: CompileView) {
var onDestroyMethod = view.destroyMethod;
if (pipeMeta.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) {
onDestroyMethod.addStmt(directiveInstance.callMethod('ngOnDestroy', []).toStmt());
onDestroyMethod.addStmt(pipeInstance.callMethod('ngOnDestroy', []).toStmt());
}
}

View File

@ -1,4 +1,5 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from '../output/output_ast';
import {
@ -7,22 +8,29 @@ import {
CompileIdentifierMetadata
} from '../compile_metadata';
import {CompileView} from './compile_view';
import {Identifiers} from '../identifiers';
export function getPropertyInView(property: o.Expression, viewPath: CompileView[]): o.Expression {
if (viewPath.length === 0) {
export function getPropertyInView(property: o.Expression, callingView: CompileView,
definedView: CompileView): o.Expression {
if (callingView === definedView) {
return property;
} else {
var viewProp: o.Expression = o.THIS_EXPR;
for (var i = 0; i < viewPath.length; i++) {
viewProp = viewProp.prop('declarationAppElement').prop('parentView');
var currView: CompileView = callingView;
while (currView !== definedView && isPresent(currView.declarationElement.view)) {
currView = currView.declarationElement.view;
viewProp = viewProp.prop('parent');
}
if (currView !== definedView) {
throw new BaseException(
`Internal error: Could not calculate a property in a parent view: ${property}`);
}
if (property instanceof o.ReadPropExpr) {
var lastView = viewPath[viewPath.length - 1];
let readPropExpr: o.ReadPropExpr = property;
// Note: Don't cast for members of the AppView base class...
if (lastView.fields.some((field) => field.name == readPropExpr.name) ||
lastView.getters.some((field) => field.name == readPropExpr.name)) {
viewProp = viewProp.cast(lastView.classType);
if (definedView.fields.some((field) => field.name == readPropExpr.name) ||
definedView.getters.some((field) => field.name == readPropExpr.name)) {
viewProp = viewProp.cast(definedView.classType);
}
}
return o.replaceVarInExpression(o.THIS_EXPR.name, viewProp, property);
@ -77,3 +85,15 @@ export function createFlatArray(expressions: o.Expression[]): o.Expression {
}
return result;
}
export function createPureProxy(fn: o.Expression, argCount: number, pureProxyProp: o.ReadPropExpr,
view: CompileView) {
view.fields.push(new o.ClassField(pureProxyProp.name, null, [o.StmtModifier.Private]));
var pureProxyId =
argCount < Identifiers.pureProxies.length ? Identifiers.pureProxies[argCount] : null;
if (isBlank(pureProxyId)) {
throw new BaseException(`Unsupported number of argument for pure functions: ${argCount}`);
}
view.createMethod.addStmt(
o.THIS_EXPR.prop(pureProxyProp.name).set(o.importExpr(pureProxyId).callFn([fn])).toStmt());
}

View File

@ -39,6 +39,8 @@ import {CompileElement, CompileNode} from './compile_element';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void {
var visitor = new ViewBinderVisitor(view);
templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
}
class ViewBinderVisitor implements TemplateAstVisitor {

View File

@ -1,4 +1,4 @@
import {isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper, SetWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
@ -429,8 +429,6 @@ function createViewClass(view: CompileView, renderCompTypeVar: o.ReadVarExpr,
ViewConstructorVars.parentInjector,
ViewConstructorVars.declarationEl,
ChangeDetectionStrategyEnum.fromValue(getChangeDetectionMode(view)),
o.literal(view.literalArrayCount),
o.literal(view.literalMapCount),
nodeDebugInfosVar
])
.toStmt()
@ -600,4 +598,4 @@ function getChangeDetectionMode(view: CompileView): ChangeDetectionStrategy {
mode = ChangeDetectionStrategy.CheckAlways;
}
return mode;
}
}