From 4d62be69c57cb69d7a761dcdc5b232fc540d4796 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Thu, 1 Feb 2018 12:13:23 -0800 Subject: [PATCH] feat(ivy): memoize array literals in render3 (#21973) PR Close #21973 --- packages/core/src/render3/index.ts | 11 + packages/core/src/render3/instructions.ts | 7 +- packages/core/src/render3/interfaces/view.ts | 3 + packages/core/src/render3/object_literal.ts | 275 ++++++++++++++++++ .../test/render3/compiler_canonical_spec.ts | 70 +++++ .../core/test/render3/object_literal_spec.ts | 253 ++++++++++++++++ 6 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/render3/object_literal.ts create mode 100644 packages/core/test/render3/object_literal_spec.ts diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 39e5f719740fd..27ca0199c42e3 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -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 diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 277ce0c2084e4..752bf18c5b9e4 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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(instanceOrArray: T | [T]): T { // Directives with content queries store an array in data[directiveIndex] // with the instance as the first index diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index d64a48922e071..e8bcca12857e2 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -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; } /** diff --git a/packages/core/src/render3/object_literal.ts b/packages/core/src/render3/object_literal.ts new file mode 100644 index 0000000000000..a99ffd025d1f1 --- /dev/null +++ b/packages/core/src/render3/object_literal.ts @@ -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; +} diff --git a/packages/core/test/render3/compiler_canonical_spec.ts b/packages/core/test/render3/compiler_canonical_spec.ts index 1c45986a19772..86a6f0e01f5a5 100644 --- a/packages/core/test/render3/compiler_canonical_spec.ts +++ b/packages/core/test/render3/compiler_canonical_spec.ts @@ -173,6 +173,76 @@ describe('compiler specification', () => { expect(log).toEqual(['ChildComponent', 'SomeDirective']); }); + describe('memoization', () => { + @Component({ + selector: 'my-comp', + template: ` +

{{ names[0] }}

+

{{ names[1] }}

+ ` + }) + 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: ` + + ` + }) + 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(`

Nancy

Bess

`); + expect(e0_literal).toEqual(['Nancy', null]); + }); + + }); + it('should support content projection', () => { @Component({selector: 'simple', template: `
`}) class SimpleComponent { diff --git a/packages/core/test/render3/object_literal_spec.ts b/packages/core/test/render3/object_literal_spec.ts new file mode 100644 index 0000000000000..a4709b555bb23 --- /dev/null +++ b/packages/core/test/render3/object_literal_spec.ts @@ -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', () => { + /** */ + 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'} + }); + } + + /** + */ + 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', () => { + /** */ + 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'} + }); + } + + /** */ + 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}); + }); + +});