Răsfoiți Sursa

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
Evan You 8 ani în urmă
părinte
comite
62a922e865

+ 17 - 9
src/core/vdom/create-functional-component.js

@@ -1,6 +1,6 @@
 /* @flow */
 /* @flow */
 
 
-import VNode from './vnode'
+import VNode, { cloneVNode } from './vnode'
 import { createElement } from './create-element'
 import { createElement } from './create-element'
 import { resolveInject } from '../instance/inject'
 import { resolveInject } from '../instance/inject'
 import { normalizeChildren } from '../vdom/helpers/normalize-children'
 import { normalizeChildren } from '../vdom/helpers/normalize-children'
@@ -32,6 +32,9 @@ export function FunctionalRenderContext (
     // $flow-disable-line
     // $flow-disable-line
     contextVm._original = parent
     contextVm._original = parent
   } else {
   } 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
     contextVm = parent
     // $flow-disable-line
     // $flow-disable-line
     parent = parent._original
     parent = parent._original
@@ -102,23 +105,28 @@ export function createFunctionalComponent (
   const vnode = options.render.call(null, renderContext._c, renderContext)
   const vnode = options.render.call(null, renderContext._c, renderContext)
 
 
   if (vnode instanceof VNode) {
   if (vnode instanceof VNode) {
-    setFunctionalContextForVNode(vnode, data, contextVm, options)
-    return vnode
+    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options)
   } else if (Array.isArray(vnode)) {
   } else if (Array.isArray(vnode)) {
     const vnodes = normalizeChildren(vnode) || []
     const vnodes = normalizeChildren(vnode) || []
+    const res = new Array(vnodes.length)
     for (let i = 0; i < vnodes.length; i++) {
     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) {
   if (data.slot) {
-    (vnode.data || (vnode.data = {})).slot = data.slot
+    (clone.data || (clone.data = {})).slot = data.slot
   }
   }
+  return clone
 }
 }
 
 
 function mergeProps (to, from) {
 function mergeProps (to, from) {

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

@@ -855,4 +855,35 @@ describe('Component slot', () => {
 
 
     expect(vm.$el.textContent).toBe('foo')
     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)
+  })
 })
 })