From 62a922e865f5e578f67b386cb614abfc173d7851 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 13 Mar 2018 17:48:52 -0400 Subject: [PATCH] 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 --- src/core/vdom/create-functional-component.js | 26 ++++++++++------ .../features/component/component-slot.spec.js | 31 +++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/core/vdom/create-functional-component.js b/src/core/vdom/create-functional-component.js index fb21cdab218..efed801db7d 100644 --- a/src/core/vdom/create-functional-component.js +++ b/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) { diff --git a/test/unit/features/component/component-slot.spec.js b/test/unit/features/component/component-slot.spec.js index 286dcb9cce9..e667a46ef89 100644 --- a/test/unit/features/component/component-slot.spec.js +++ b/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: `
foo
`, + components: { Stateful } + }).$mount() + + expect(vm.$el.textContent).toBe('foo') + vm.$refs.stateful.ok = false + waitForUpdate(() => { + expect(vm.$el.textContent).toBe('foo') + }).then(done) + }) })