feat: auto cache inline prop literals to avoid child re-render

This commit is contained in:
Evan You 2017-12-18 12:55:02 -05:00
parent f493715f39
commit 996eb00a0a
6 changed files with 83 additions and 0 deletions

View File

@ -69,6 +69,7 @@ declare interface Component {
_staticTrees: ?Array<VNode>; // 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<number>, 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;

View File

@ -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)
)) {

View File

@ -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
* <foo :bar="{ a: 1 }"/>
* to:
* <foo :bar="_a(0, function(){return { a: 1 }})"
*/
export function createInlineComputed (id: string, getter: Function): any {
const vm: Component = this
const watchers = vm._inlineComputed || (vm._inlineComputed = {})
const cached = watchers[id]
if (cached) {
return cached.value
} else {
watchers[id] = new Watcher(vm, getter, noop, { sync: true })
return watchers[id].value
}
}

View File

@ -10,6 +10,7 @@ import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'
import { createInlineComputed } from './create-inline-computed'
export function installRenderHelpers (target: any) {
target._o = markOnce
@ -27,4 +28,5 @@ export function installRenderHelpers (target: any) {
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._a = createInlineComputed
}

View File

@ -47,6 +47,7 @@ export function proxy (target: Object, sourceKey: string, key: string) {
export function initState (vm: Component) {
vm._watchers = []
vm._inlineComputed = null
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)

View File

@ -529,4 +529,38 @@ describe('Options props', () => {
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: `
<div id="app">
{{ n }} {{ m }} <foo :a="{ n: 1 }" :b="{ n: n }"/>
</div>
`,
components: {
foo: {
props: ['a', 'b'],
updated,
template: `<div>{{ a.n }} {{ b.n }}</div>`
}
}
}).$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)
})
})