From 996eb00a0a0933376c9364c2d187e2bf0512ff0d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Dec 2017 12:55:02 -0500 Subject: [PATCH] feat: auto cache inline prop literals to avoid child re-render --- flow/component.js | 3 ++ src/compiler/parser/index.js | 15 ++++++++ .../render-helpers/create-inline-computed.js | 28 +++++++++++++++ src/core/instance/render-helpers/index.js | 2 ++ src/core/instance/state.js | 1 + test/unit/features/options/props.spec.js | 34 +++++++++++++++++++ 6 files changed, 83 insertions(+) create mode 100644 src/core/instance/render-helpers/create-inline-computed.js diff --git a/flow/component.js b/flow/component.js index 9fbade0b..41596777 100644 --- a/flow/component.js +++ b/flow/component.js @@ -69,6 +69,7 @@ declare interface Component { _staticTrees: ?Array; // v-once cached trees _hasHookEvent: boolean; _provided: ?Object; + _inlineComputed: ?{ [key: string]: Watcher }; // inline computed watchers for literal props // private methods @@ -129,6 +130,8 @@ declare interface Component { _k: (eventKeyCode: number, key: string, builtInAlias?: number | Array, eventKeyName?: string) => ?boolean; // resolve scoped slots _u: (scopedSlots: ScopedSlotsData, res?: Object) => { [key: string]: Function }; + // create / return value from inline computed + _a: (id: number, getter: Function) => any; // SSR specific _ssrNode: Function; diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index 7f9bc1b5..a2476dbb 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -29,16 +29,20 @@ const argRE = /:(.*)$/ const bindRE = /^:|^v-bind:/ const modifierRE = /\.[^.]+/g +const literalValueRE = /^(\{.*\}|\[.*\])$/ + const decodeHTMLCached = cached(he.decode) // configurable state export let warn: any +let literalPropId let delimiters let transforms let preTransforms let postTransforms let platformIsPreTag let platformMustUseProp +let platformIsReservedTag let platformGetTagNamespace type Attr = { name: string; value: string }; @@ -66,9 +70,11 @@ export function parse ( options: CompilerOptions ): ASTElement | void { warn = options.warn || baseWarn + literalPropId = 0 platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no + platformIsReservedTag = options.isReservedTag || no platformGetTagNamespace = options.getTagNamespace || no transforms = pluckModuleFunction(options.modules, 'transformNode') @@ -529,6 +535,15 @@ function processAttrs (el) { ) } } + // optimize literal values in component props by wrapping them + // in an inline watcher to avoid unnecessary re-renders + if ( + !platformIsReservedTag(el.tag) && + el.tag !== 'slot' && + literalValueRE.test(value.trim()) + ) { + value = `_a(${literalPropId++},function(){return ${value}})` + } if (isProp || ( !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) { diff --git a/src/core/instance/render-helpers/create-inline-computed.js b/src/core/instance/render-helpers/create-inline-computed.js new file mode 100644 index 00000000..8413eb0e --- /dev/null +++ b/src/core/instance/render-helpers/create-inline-computed.js @@ -0,0 +1,28 @@ +/* @flow */ + +import { noop } from 'shared/util' +import Watcher from 'core/observer/watcher' + +/** + * This runtime helper creates an inline computed property for component + * props that contain object or array literals. The caching ensures the same + * object/array is returned unless the value has indeed changed, thus avoiding + * the child component to always re-render when comparing props values. + * + * Installed to the instance as _a, requires special handling in parser that + * transforms the following + * + * to: + * { expect(`Invalid key "reqquired" in validation rules object for prop "value".`).toHaveBeenWarned() expect(`Invalid key "deafult" in validation rules object for prop "count".`).toHaveBeenWarned() }) + + it('should not trigger re-render on non-changed inline literals', done => { + const updated = jasmine.createSpy('updated') + const vm = new Vue({ + data: { + n: 1, + m: 1 + }, + template: ` +
+ {{ n }} {{ m }} +
+ `, + components: { + foo: { + props: ['a', 'b'], + updated, + template: `
{{ a.n }} {{ b.n }}
` + } + } + }).$mount() + + expect(vm.$el.textContent).toContain('1 1 1 1') + vm.n++ // literals that actually contain changed reactive data should trigger update + waitForUpdate(() => { + expect(vm.$el.textContent).toContain('2 1 1 2') + expect(updated.calls.count()).toBe(1) + }).then(() => { + vm.m++ // changing data that does not affect any literals should not trigger update + }).then(() => { + expect(vm.$el.textContent).toContain('2 2 1 2') + expect(updated.calls.count()).toBe(1) + }).then(done) + }) })