Просмотр исходного кода

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 лет назад
Родитель
Сommit
62a922e865

+ 17 - 9
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 - 0
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)
+  })
 })