Browse Source

fix: fix wrongly matched named slots in functional components

This is a subtle edge case caused when a stateful component triggers
a self re-render, which reuses cached slot nodes. The cached slot
nodes, if returned from a functional render fn, gets the fnContext
property which causes subsequent slot resolving to not function
properly. To fix this, nodes returned from functional components
need to be cloned before getting assigned fnContext.

fix #7817
dev
Evan You 7 years ago
parent
commit
62a922e865
  1. 26
      src/core/vdom/create-functional-component.js
  2. 31
      test/unit/features/component/component-slot.spec.js

26
src/core/vdom/create-functional-component.js

@ -1,6 +1,6 @@
/* @flow */
import VNode from './vnode'
import VNode, { cloneVNode } from './vnode'
import { createElement } from './create-element'
import { resolveInject } from '../instance/inject'
import { normalizeChildren } from '../vdom/helpers/normalize-children'
@ -32,6 +32,9 @@ export function FunctionalRenderContext (
// $flow-disable-line
contextVm._original = parent
} else {
// the context vm passed in is a functional context as well.
// in this case we want to make sure we are able to get a hold to the
// real context instance.
contextVm = parent
// $flow-disable-line
parent = parent._original
@ -102,23 +105,28 @@ export function createFunctionalComponent (
const vnode = options.render.call(null, renderContext._c, renderContext)
if (vnode instanceof VNode) {
setFunctionalContextForVNode(vnode, data, contextVm, options)
return vnode
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options)
} else if (Array.isArray(vnode)) {
const vnodes = normalizeChildren(vnode) || []
const res = new Array(vnodes.length)
for (let i = 0; i < vnodes.length; i++) {
setFunctionalContextForVNode(vnodes[i], data, contextVm, options)
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options)
}
return vnodes
return res
}
}
function setFunctionalContextForVNode (vnode, data, vm, options) {
vnode.fnContext = vm
vnode.fnOptions = options
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options) {
// #7817 clone node before setting fnContext, otherwise if the node is reused
// (e.g. it was from a cached normal slot) the fnContext causes named slots
// that should not be matched to match.
const clone = cloneVNode(vnode)
clone.fnContext = contextVm
clone.fnOptions = options
if (data.slot) {
(vnode.data || (vnode.data = {})).slot = data.slot
(clone.data || (clone.data = {})).slot = data.slot
}
return clone
}
function mergeProps (to, from) {

31
test/unit/features/component/component-slot.spec.js

@ -855,4 +855,35 @@ describe('Component slot', () => {
expect(vm.$el.textContent).toBe('foo')
})
// #7817
it('should not match wrong named slot in functional component on re-render', done => {
const Functional = {
functional: true,
render: (h, ctx) => ctx.slots().default
}
const Stateful = {
data () {
return { ok: true }
},
render (h) {
this.ok // register dep
return h('div', [
h(Functional, this.$slots.named)
])
}
}
const vm = new Vue({
template: `<stateful ref="stateful"><div slot="named">foo</div></stateful>`,
components: { Stateful }
}).$mount()
expect(vm.$el.textContent).toBe('foo')
vm.$refs.stateful.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('foo')
}).then(done)
})
})

Loading…
Cancel
Save