Skip to content

Commit

Permalink
feat: auto cache inline prop literals to avoid child re-render
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Dec 18, 2017
1 parent f493715 commit 996eb00
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 0 deletions.
3 changes: 3 additions & 0 deletions flow/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
)) {
Expand Down
28 changes: 28 additions & 0 deletions src/core/instance/render-helpers/create-inline-computed.js
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 2 additions & 0 deletions src/core/instance/render-helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,4 +28,5 @@ export function installRenderHelpers (target: any) {
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._a = createInlineComputed
}
1 change: 1 addition & 0 deletions src/core/instance/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions test/unit/features/options/props.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

3 comments on commit 996eb00

@sirlancelot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the inline computed watchers need to have a teardown as well or will the Garbage Collector pick them up automatically?

@yyx990803
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are some neglected issues with this strategy. I'm reverting the change for now.

@trestletech
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yyx990803 we had a build with buggy behavior (links created inside of a v-for weren't properly resolving to the appropriate target) when built on 2.5.12 which had this code. This bug fixed itself when we built against 2.5.13 after this had been resolved.

Our code which started misbehaving:

<router-link v-for="menuItem in menuItems" :to="{ name: menuItem.routeName, params: menuItem.routeParams }" class="menuItem" key="menuItem.routeName">
              {{ menuItem.label }}
</router-link>

Let me know if we can provide any more useful information to help add a test case to cover this in the future.

As an aside: it was a bit tough to track this down looking through the commit log. Is there a changelog where we could find a description of changes like this for future reference?

Thanks!

Please sign in to comment.