Skip to content

Commit

Permalink
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
@@ -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;
15 changes: 15 additions & 0 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
@@ -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)
)) {
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
@@ -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
}
1 change: 1 addition & 0 deletions src/core/instance/state.js
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions test/unit/features/options/props.spec.js
Original file line number Diff line number Diff line change
@@ -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.