Skip to content

Commit

Permalink
fix: fix wrongly matched named slots in functional components
Browse files Browse the repository at this point in the history
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 vuejs#7817
  • Loading branch information
yyx990803 authored and hefeng committed Jan 25, 2019
1 parent 25b4cb2 commit bf41af0
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 9 deletions.
26 changes: 17 additions & 9 deletions 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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 31 additions & 0 deletions test/unit/features/component/component-slot.spec.js
Expand Up @@ -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)
})
})

0 comments on commit bf41af0

Please sign in to comment.