feat(ivy): memoize array literals in render3 (#21973)

PR Close #21973
This commit is contained in:
Kara Erickson 2018-02-01 12:13:23 -08:00 committed by Alex Rickabaugh
parent 7e51e52f55
commit 4d62be69c5
6 changed files with 618 additions and 1 deletions

View File

@ -78,6 +78,17 @@ export {
query as Q,
queryRefresh as qR,
} from './query';
export {
objectLiteral1 as o1,
objectLiteral2 as o2,
objectLiteral3 as o3,
objectLiteral4 as o4,
objectLiteral5 as o5,
objectLiteral6 as o6,
objectLiteral7 as o7,
objectLiteral8 as o8,
} from './object_literal';
// clang-format on

View File

@ -494,7 +494,8 @@ export function createTView(): TView {
contentCheckHooks: null,
viewHooks: null,
viewCheckHooks: null,
destroyHooks: null
destroyHooks: null,
objectLiterals: null
};
}
@ -1754,6 +1755,10 @@ export function getRenderer(): Renderer3 {
return renderer;
}
export function getTView(): TView {
return currentView.tView;
}
export function getDirectiveInstance<T>(instanceOrArray: T | [T]): T {
// Directives with content queries store an array in data[directiveIndex]
// with the instance as the first index

View File

@ -259,6 +259,9 @@ export interface TView {
* Odd indices: Hook function
*/
destroyHooks: HookData|null;
/** Contains copies of object literals that were passed as bindings in this view. */
objectLiterals: any[]|null;
}
/**

View File

@ -0,0 +1,275 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertEqual} from './assert';
import {NO_CHANGE, bind, getTView} from './instructions';
/**
* Updates an expression in an object literal if the expression has changed.
* Used in objectLiteral instructions.
*
* @param obj Object to update
* @param key Key to set in object
* @param exp Expression to set at key
* @returns Whether or not there has been a change
*/
function updateBinding(obj: any, key: string | number, exp: any): boolean {
if (bind(exp) !== NO_CHANGE) {
obj[key] = exp;
return true;
}
return false;
}
/** Updates two expressions in an object literal if they have changed. */
function updateBinding2(obj: any, key1: number, exp1: any, key2: number, exp2: any): boolean {
let different = updateBinding(obj, key1, exp1);
return updateBinding(obj, key2, exp2) || different;
}
/** Updates four expressions in an object literal if they have changed. */
function updateBinding4(
obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, exp3: any,
key4: number, exp4: any): boolean {
let different = updateBinding2(obj, key1, exp1, key2, exp2);
return updateBinding2(obj, key3, exp3, key4, exp4) || different;
}
/**
* Gets a blueprint of an object or array if one has already been saved, or copies the
* object and saves it for the next change detection run if it hasn't.
*/
function getMutableBlueprint(index: number, obj: any): any {
const tView = getTView();
const objectLiterals = tView.objectLiterals;
if (objectLiterals && index < objectLiterals.length) {
return objectLiterals[index];
} else {
ngDevMode && objectLiterals && assertEqual(index, objectLiterals.length, 'index');
return (objectLiterals || (tView.objectLiterals = []))[index] = copyObject(obj);
}
}
/** Copies an object or array */
function copyObject(obj: any): any {
return Array.isArray(obj) ? obj.slice() : {...obj};
}
/**
* Updates the expression in the given object if it has changed and returns a copy of the object.
* Or if the expression hasn't changed, returns NO_CHANGE.
*
* @param objIndex Index of object blueprint in objectLiterals
* @param obj Object to update
* @param key Key to set in object
* @param exp Expression to set at key
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral1(objIndex: number, obj: any, key: string | number, exp: any): any {
obj = getMutableBlueprint(objIndex, obj);
if (bind(exp) === NO_CHANGE) {
return NO_CHANGE;
} else {
obj[key] = exp;
// Must copy to change identity when binding changes
return copyObject(obj);
}
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @returns A copy of the array or NO_CHANGE
*/
export function objectLiteral2(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any): any {
obj = getMutableBlueprint(objIndex, obj);
return updateBinding2(obj, key1, exp1, key2, exp2) ? copyObject(obj) : NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral3(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any): any {
obj = getMutableBlueprint(objIndex, obj);
let different = updateBinding2(obj, key1, exp1, key2, exp2);
return updateBinding(obj, key3, exp3) || different ? copyObject(obj) : NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @param key4
* @param exp4
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral4(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any, key4: number, exp4: any): any {
obj = getMutableBlueprint(objIndex, obj);
return updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4) ? copyObject(obj) :
NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @param key4
* @param exp4
* @param key5
* @param exp5
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral5(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any, key4: number, exp4: any, key5: number, exp5: any): any {
obj = getMutableBlueprint(objIndex, obj);
let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4);
return updateBinding(obj, key5, exp5) || different ? copyObject(obj) : NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @param key4
* @param exp4
* @param key5
* @param exp5
* @param key6
* @param exp6
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral6(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any): any {
obj = getMutableBlueprint(objIndex, obj);
let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4);
return updateBinding2(obj, key5, exp5, key6, exp6) || different ? copyObject(obj) : NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @param key4
* @param exp4
* @param key5
* @param exp5
* @param key6
* @param exp6
* @param key7
* @param exp7
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral7(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any,
key7: number, exp7: any): any {
obj = getMutableBlueprint(objIndex, obj);
let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4);
different = updateBinding2(obj, key5, exp5, key6, exp6) || different;
return updateBinding(obj, key7, exp7) || different ? copyObject(obj) : NO_CHANGE;
}
/**
* Updates the expressions in the given object if they have changed and returns a copy of the
* object.
* Or if no expressions have changed, returns NO_CHANGE.
*
* @param objIndex
* @param obj
* @param key1
* @param exp1
* @param key2
* @param exp2
* @param key3
* @param exp3
* @param key4
* @param exp4
* @param key5
* @param exp5
* @param key6
* @param exp6
* @param key7
* @param exp7
* @param key8
* @param exp8
* @returns A copy of the object or NO_CHANGE
*/
export function objectLiteral8(
objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number,
exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any,
key7: number, exp7: any, key8: number, exp8: any): any {
obj = getMutableBlueprint(objIndex, obj);
let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4);
return updateBinding4(obj, key5, exp5, key6, exp6, key7, exp7, key8, exp8) || different ?
copyObject(obj) :
NO_CHANGE;
}

View File

@ -173,6 +173,76 @@ describe('compiler specification', () => {
expect(log).toEqual(['ChildComponent', 'SomeDirective']);
});
describe('memoization', () => {
@Component({
selector: 'my-comp',
template: `
<p>{{ names[0] }}</p>
<p>{{ names[1] }}</p>
`
})
class MyComp {
@Input() names: string[];
static ngComponentDef = r3.defineComponent({
type: MyComp,
tag: 'my-comp',
factory: function MyComp_Factory() { return new MyComp(); },
template: function MyComp_Template(ctx: MyComp, cm: boolean) {
if (cm) {
r3.E(0, 'p');
r3.T(1);
r3.e();
r3.E(2, 'p');
r3.T(3);
r3.e();
}
r3.t(1, r3.b(ctx.names[0]));
r3.t(3, r3.b(ctx.names[1]));
},
inputs: {names: 'names'}
});
}
it('should memoize array literals', () => {
@Component({
selector: 'my-app',
template: `
<my-comp [names]="['Nancy', customName]"></my-comp>
`
})
class MyApp {
customName = 'Bess';
// NORMATIVE
static ngComponentDef = r3.defineComponent({
type: MyApp,
tag: 'my-app',
factory: function MyApp_Factory() { return new MyApp(); },
template: function MyApp_Template(ctx: MyApp, cm: boolean) {
if (cm) {
r3.E(0, MyComp);
r3.e();
}
r3.p(0, 'names', r3.o1(0, e0_literal, 1, ctx.customName));
MyComp.ngComponentDef.h(1, 0);
r3.r(1, 0);
}
});
// /NORMATIVE
}
// NORMATIVE
const e0_literal = ['Nancy', null];
// /NORMATIVE
expect(renderComp(MyApp)).toEqual(`<my-comp><p>Nancy</p><p>Bess</p></my-comp>`);
expect(e0_literal).toEqual(['Nancy', null]);
});
});
it('should support content projection', () => {
@Component({selector: 'simple', template: `<div><ng-content></ng-content></div>`})
class SimpleComponent {

View File

@ -0,0 +1,253 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {E, defineComponent, e, m, o1, o2, o3, o4, o5, o6, o7, o8, p, r} from '../../src/render3/index';
import {renderToHtml} from '../../test/render3/render_util';
describe('array literals', () => {
let myComp: MyComp;
class MyComp {
names: string[];
static ngComponentDef = defineComponent({
type: MyComp,
tag: 'my-comp',
factory: function MyComp_Factory() { return myComp = new MyComp(); },
template: function MyComp_Template(ctx: MyComp, cm: boolean) {},
inputs: {names: 'names'}
});
}
it('should support an array literal with a binding', () => {
/** <my-comp [names]="['Nancy', customName, 'Bess']"></my-comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComp);
e();
}
p(0, 'names', o1(0, e0_literal, 1, ctx.customName));
MyComp.ngComponentDef.h(1, 0);
r(1, 0);
}
const e0_literal = ['Nancy', null, 'Bess'];
renderToHtml(Template, {customName: 'Carson'});
const firstArray = myComp !.names;
expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess']);
renderToHtml(Template, {customName: 'Carson'});
expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess']);
expect(firstArray).toBe(myComp !.names);
renderToHtml(Template, {customName: 'Hannah'});
expect(myComp !.names).toEqual(['Nancy', 'Hannah', 'Bess']);
// Identity must change if binding changes
expect(firstArray).not.toBe(myComp !.names);
expect(e0_literal).toEqual(['Nancy', null, 'Bess']);
});
it('should support multiple array literals passed through to one node', () => {
let manyPropComp: ManyPropComp;
class ManyPropComp {
names1: string[];
names2: string[];
static ngComponentDef = defineComponent({
type: ManyPropComp,
tag: 'many-prop-comp',
factory: function ManyPropComp_Factory() { return manyPropComp = new ManyPropComp(); },
template: function ManyPropComp_Template(ctx: ManyPropComp, cm: boolean) {},
inputs: {names1: 'names1', names2: 'names2'}
});
}
/** <many-prop-comp [names1]="['Nancy', customName]" [names2]="[customName2]"></many-prop-comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ManyPropComp);
e();
}
p(0, 'names1', o1(0, e0_literal, 1, ctx.customName));
p(0, 'names2', o1(1, e0_literal_1, 0, ctx.customName2));
ManyPropComp.ngComponentDef.h(1, 0);
r(1, 0);
}
const e0_literal = ['Nancy', null];
const e0_literal_1 = [null];
renderToHtml(Template, {customName: 'Carson', customName2: 'George'});
expect(manyPropComp !.names1).toEqual(['Nancy', 'Carson']);
expect(manyPropComp !.names2).toEqual(['George']);
renderToHtml(Template, {customName: 'George', customName2: 'Carson'});
expect(manyPropComp !.names1).toEqual(['Nancy', 'George']);
expect(manyPropComp !.names2).toEqual(['Carson']);
expect(e0_literal).toEqual(['Nancy', null]);
expect(e0_literal_1).toEqual([null]);
});
it('should support an array literal with more than 1 binding', () => {
/** <my-comp [names]="['Nancy', customName, 'Bess', customName2]"></my-comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, MyComp);
e();
}
p(0, 'names', o2(0, e0_literal, 1, ctx.customName, 3, ctx.customName2));
MyComp.ngComponentDef.h(1, 0);
r(1, 0);
}
const e0_literal = ['Nancy', null, 'Bess', null];
renderToHtml(Template, {customName: 'Carson', customName2: 'Hannah'});
const firstArray = myComp !.names;
expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']);
renderToHtml(Template, {customName: 'Carson', customName2: 'Hannah'});
expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']);
expect(firstArray).toBe(myComp !.names);
renderToHtml(Template, {customName: 'George', customName2: 'Hannah'});
expect(myComp !.names).toEqual(['Nancy', 'George', 'Bess', 'Hannah']);
expect(firstArray).not.toBe(myComp !.names);
renderToHtml(Template, {customName: 'Frank', customName2: 'Ned'});
expect(myComp !.names).toEqual(['Nancy', 'Frank', 'Bess', 'Ned']);
});
it('should work up to 8 bindings', () => {
let o3Comp: MyComp;
let o4Comp: MyComp;
let o5Comp: MyComp;
let o6Comp: MyComp;
let o7Comp: MyComp;
let o8Comp: MyComp;
function Template(c: any, cm: boolean) {
if (cm) {
E(0, MyComp);
o3Comp = m(1);
e();
E(2, MyComp);
o4Comp = m(3);
e();
E(4, MyComp);
o5Comp = m(5);
e();
E(6, MyComp);
o6Comp = m(7);
e();
E(8, MyComp);
o7Comp = m(9);
e();
E(10, MyComp);
o8Comp = m(11);
e();
}
p(0, 'names', o3(0, e0_literal, 5, c[5], 6, c[6], 7, c[7]));
p(2, 'names', o4(1, e2_literal, 4, c[4], 5, c[5], 6, c[6], 7, c[7]));
p(4, 'names', o5(2, e4_literal, 3, c[3], 4, c[4], 5, c[5], 6, c[6], 7, c[7]));
p(6, 'names', o6(3, e6_literal, 2, c[2], 3, c[3], 4, c[4], 5, c[5], 6, c[6], 7, c[7]));
p(8, 'names',
o7(4, e8_literal, 1, c[1], 2, c[2], 3, c[3], 4, c[4], 5, c[5], 6, c[6], 7, c[7]));
p(10, 'names',
o8(5, e10_literal, 0, c[0], 1, c[1], 2, c[2], 3, c[3], 4, c[4], 5, c[5], 6, c[6], 7, c[7]));
MyComp.ngComponentDef.h(1, 0);
r(1, 0);
MyComp.ngComponentDef.h(3, 2);
r(3, 2);
MyComp.ngComponentDef.h(5, 4);
r(5, 4);
MyComp.ngComponentDef.h(7, 6);
r(7, 6);
MyComp.ngComponentDef.h(9, 8);
r(9, 8);
MyComp.ngComponentDef.h(11, 10);
r(11, 10);
}
const e0_literal = ['a', 'b', 'c', 'd', 'e', null, null, null];
const e2_literal = ['a', 'b', 'c', 'd', null, null, null, null];
const e4_literal = ['a', 'b', 'c', null, null, null, null, null];
const e6_literal = ['a', 'b', null, null, null, null, null, null];
const e8_literal = ['a', null, null, null, null, null, null, null];
const e10_literal = [null, null, null, null, null, null, null, null];
renderToHtml(Template, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']);
expect(o3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
expect(o4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
expect(o5Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
expect(o6Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
expect(o7Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
expect(o8Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
renderToHtml(Template, ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1', 'i1']);
expect(o3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h1']);
expect(o4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h1']);
expect(o5Comp !.names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h1']);
expect(o6Comp !.names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']);
expect(o7Comp !.names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']);
expect(o8Comp !.names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']);
});
it('should support an object literal', () => {
let objectComp: ObjectComp;
class ObjectComp {
config: {[key: string]: any};
static ngComponentDef = defineComponent({
type: ObjectComp,
tag: 'object-comp',
factory: function ObjectComp_Factory() { return objectComp = new ObjectComp(); },
template: function ObjectComp_Template(ctx: ObjectComp, cm: boolean) {},
inputs: {config: 'config'}
});
}
/** <object-comp [config]="{duration: 500, animation: ctx.name}"></object-comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, ObjectComp);
e();
}
p(0, 'config', o1(0, e0_literal, 'animation', ctx.name));
ObjectComp.ngComponentDef.h(1, 0);
r(1, 0);
}
const e0_literal = {duration: 500, animation: null};
renderToHtml(Template, {name: 'slide'});
const firstObj = objectComp !.config;
expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'});
renderToHtml(Template, {name: 'slide'});
expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'});
expect(firstObj).toBe(objectComp !.config);
renderToHtml(Template, {name: 'tap'});
expect(objectComp !.config).toEqual({duration: 500, animation: 'tap'});
// Identity must change if binding changes
expect(firstObj).not.toBe(objectComp !.config);
expect(e0_literal).toEqual({duration: 500, animation: null});
});
});