Skip to content

Commit 62a922e

Browse files
committed
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
1 parent 9084747 commit 62a922e

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

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

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @flow */
22

3-
import VNode from './vnode'
3+
import VNode, { cloneVNode } from './vnode'
44
import { createElement } from './create-element'
55
import { resolveInject } from '../instance/inject'
66
import { normalizeChildren } from '../vdom/helpers/normalize-children'
@@ -32,6 +32,9 @@ export function FunctionalRenderContext (
3232
// $flow-disable-line
3333
contextVm._original = parent
3434
} else {
35+
// the context vm passed in is a functional context as well.
36+
// in this case we want to make sure we are able to get a hold to the
37+
// real context instance.
3538
contextVm = parent
3639
// $flow-disable-line
3740
parent = parent._original
@@ -102,23 +105,28 @@ export function createFunctionalComponent (
102105
const vnode = options.render.call(null, renderContext._c, renderContext)
103106

104107
if (vnode instanceof VNode) {
105-
setFunctionalContextForVNode(vnode, data, contextVm, options)
106-
return vnode
108+
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options)
107109
} else if (Array.isArray(vnode)) {
108110
const vnodes = normalizeChildren(vnode) || []
111+
const res = new Array(vnodes.length)
109112
for (let i = 0; i < vnodes.length; i++) {
110-
setFunctionalContextForVNode(vnodes[i], data, contextVm, options)
113+
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options)
111114
}
112-
return vnodes
115+
return res
113116
}
114117
}
115118

116-
function setFunctionalContextForVNode (vnode, data, vm, options) {
117-
vnode.fnContext = vm
118-
vnode.fnOptions = options
119+
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options) {
120+
// #7817 clone node before setting fnContext, otherwise if the node is reused
121+
// (e.g. it was from a cached normal slot) the fnContext causes named slots
122+
// that should not be matched to match.
123+
const clone = cloneVNode(vnode)
124+
clone.fnContext = contextVm
125+
clone.fnOptions = options
119126
if (data.slot) {
120-
(vnode.data || (vnode.data = {})).slot = data.slot
127+
(clone.data || (clone.data = {})).slot = data.slot
121128
}
129+
return clone
122130
}
123131

124132
function mergeProps (to, from) {

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

+31
Original file line numberDiff line numberDiff line change
@@ -855,4 +855,35 @@ describe('Component slot', () => {
855855

856856
expect(vm.$el.textContent).toBe('foo')
857857
})
858+
859+
// #7817
860+
it('should not match wrong named slot in functional component on re-render', done => {
861+
const Functional = {
862+
functional: true,
863+
render: (h, ctx) => ctx.slots().default
864+
}
865+
866+
const Stateful = {
867+
data () {
868+
return { ok: true }
869+
},
870+
render (h) {
871+
this.ok // register dep
872+
return h('div', [
873+
h(Functional, this.$slots.named)
874+
])
875+
}
876+
}
877+
878+
const vm = new Vue({
879+
template: `<stateful ref="stateful"><div slot="named">foo</div></stateful>`,
880+
components: { Stateful }
881+
}).$mount()
882+
883+
expect(vm.$el.textContent).toBe('foo')
884+
vm.$refs.stateful.ok = false
885+
waitForUpdate(() => {
886+
expect(vm.$el.textContent).toBe('foo')
887+
}).then(done)
888+
})
858889
})

0 commit comments

Comments
 (0)