Browse Source

fix: async component should use render owner as force update context

Previously, an async component uses its lexical owner as the force
update context. This works when the async component is rendered in a
scoped slot because in the past parent components always force update
child components with any type of slots. After the optimization in
f219bed though, child components with only scoped slots are no longer
force-updated, and this cause async components inside scoped slots to
not trigger the proper update. Turns out they should have used the
actual render owner (the component that invokes the scoped slot) as the
force update context all along.

fix #9432
Evan You 7 years ago
parent
commit
b9de23b100

+ 13 - 0
src/core/instance/render.js

@@ -51,6 +51,13 @@ export function initRender (vm: Component) {
   }
   }
 }
 }
 
 
+export let currentRenderingInstance: Component | null = null
+
+// for testing only
+export function setCurrentRenderingInstance (vm: Component) {
+  currentRenderingInstance = vm
+}
+
 export function renderMixin (Vue: Class<Component>) {
 export function renderMixin (Vue: Class<Component>) {
   // install runtime convenience helpers
   // install runtime convenience helpers
   installRenderHelpers(Vue.prototype)
   installRenderHelpers(Vue.prototype)
@@ -76,6 +83,10 @@ export function renderMixin (Vue: Class<Component>) {
     // render self
     // render self
     let vnode
     let vnode
     try {
     try {
+      // There's no need to maintain a stack becaues all render fns are called
+      // separately from one another. Nested component's render fns are called
+      // when parent component is patched.
+      currentRenderingInstance = vm
       vnode = render.call(vm._renderProxy, vm.$createElement)
       vnode = render.call(vm._renderProxy, vm.$createElement)
     } catch (e) {
     } catch (e) {
       handleError(e, vm, `render`)
       handleError(e, vm, `render`)
@@ -92,6 +103,8 @@ export function renderMixin (Vue: Class<Component>) {
       } else {
       } else {
         vnode = vm._vnode
         vnode = vm._vnode
       }
       }
+    } finally {
+      currentRenderingInstance = null
     }
     }
     // if the returned array contains only a single node, allow it
     // if the returned array contains only a single node, allow it
     if (Array.isArray(vnode) && vnode.length === 1) {
     if (Array.isArray(vnode) && vnode.length === 1) {

+ 1 - 1
src/core/vdom/create-component.js

@@ -129,7 +129,7 @@ export function createComponent (
   let asyncFactory
   let asyncFactory
   if (isUndef(Ctor.cid)) {
   if (isUndef(Ctor.cid)) {
     asyncFactory = Ctor
     asyncFactory = Ctor
-    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
+    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
     if (Ctor === undefined) {
     if (Ctor === undefined) {
       // return a placeholder node for async component, which is rendered
       // return a placeholder node for async component, which is rendered
       // as a comment node but preserves all the raw information for the node.
       // as a comment node but preserves all the raw information for the node.

+ 10 - 9
src/core/vdom/helpers/resolve-async-component.js

@@ -12,6 +12,7 @@ import {
 } from 'core/util/index'
 } from 'core/util/index'
 
 
 import { createEmptyVNode } from 'core/vdom/vnode'
 import { createEmptyVNode } from 'core/vdom/vnode'
+import { currentRenderingInstance } from 'core/instance/render'
 
 
 function ensureCtor (comp: any, base) {
 function ensureCtor (comp: any, base) {
   if (
   if (
@@ -40,8 +41,7 @@ export function createAsyncPlaceholder (
 
 
 export function resolveAsyncComponent (
 export function resolveAsyncComponent (
   factory: Function,
   factory: Function,
-  baseCtor: Class<Component>,
-  context: Component
+  baseCtor: Class<Component>
 ): Class<Component> | void {
 ): Class<Component> | void {
   if (isTrue(factory.error) && isDef(factory.errorComp)) {
   if (isTrue(factory.error) && isDef(factory.errorComp)) {
     return factory.errorComp
     return factory.errorComp
@@ -55,20 +55,21 @@ export function resolveAsyncComponent (
     return factory.loadingComp
     return factory.loadingComp
   }
   }
 
 
-  if (isDef(factory.contexts)) {
+  const owner = currentRenderingInstance
+  if (isDef(factory.owners)) {
     // already pending
     // already pending
-    factory.contexts.push(context)
+    factory.owners.push(owner)
   } else {
   } else {
-    const contexts = factory.contexts = [context]
+    const owners = factory.owners = [owner]
     let sync = true
     let sync = true
 
 
     const forceRender = (renderCompleted: boolean) => {
     const forceRender = (renderCompleted: boolean) => {
-      for (let i = 0, l = contexts.length; i < l; i++) {
-        contexts[i].$forceUpdate()
+      for (let i = 0, l = owners.length; i < l; i++) {
+        (owners[i]: any).$forceUpdate()
       }
       }
 
 
       if (renderCompleted) {
       if (renderCompleted) {
-        contexts.length = 0
+        owners.length = 0
       }
       }
     }
     }
 
 
@@ -80,7 +81,7 @@ export function resolveAsyncComponent (
       if (!sync) {
       if (!sync) {
         forceRender(true)
         forceRender(true)
       } else {
       } else {
-        contexts.length = 0
+        owners.length = 0
       }
       }
     })
     })
 
 

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

@@ -935,4 +935,38 @@ describe('Component scoped slot', () => {
       expect(childUpdate.calls.count()).toBe(1)
       expect(childUpdate.calls.count()).toBe(1)
     }).then(done)
     }).then(done)
   })
   })
+
+  // #9432: async components inside a scoped slot should trigger update of the
+  // component that invoked the scoped slot, not the lexical context component.
+  it('async component inside scoped slot', done => {
+    let p
+    const vm = new Vue({
+      template: `
+        <foo>
+          <template #default>
+            <bar />
+          </template>
+        </foo>
+      `,
+      components: {
+        foo: {
+          template: `<div>foo<slot/></div>`
+        },
+        bar: resolve => {
+          setTimeout(() => {
+            resolve({
+              template: `<div>bar</div>`
+            })
+            next()
+          }, 0)
+        }
+      }
+    }).$mount()
+
+    function next () {
+      waitForUpdate(() => {
+        expect(vm.$el.textContent).toBe(`foobar`)
+      }).then(done)
+    }
+  })
 })
 })

+ 8 - 3
test/unit/modules/vdom/create-component.spec.js

@@ -1,5 +1,6 @@
 import Vue from 'vue'
 import Vue from 'vue'
 import { createComponent } from 'core/vdom/create-component'
 import { createComponent } from 'core/vdom/create-component'
+import { setCurrentRenderingInstance } from 'core/instance/render'
 
 
 describe('create-component', () => {
 describe('create-component', () => {
   let vm
   let vm
@@ -55,13 +56,17 @@ describe('create-component', () => {
       }, 0)
       }, 0)
     }
     }
     function go () {
     function go () {
+      setCurrentRenderingInstance(vm)
       vnode = createComponent(async, data, vm, vm)
       vnode = createComponent(async, data, vm, vm)
+      setCurrentRenderingInstance(null)
       expect(vnode.isComment).toBe(true) // not to be loaded yet.
       expect(vnode.isComment).toBe(true) // not to be loaded yet.
       expect(vnode.asyncFactory).toBe(async)
       expect(vnode.asyncFactory).toBe(async)
-      expect(vnode.asyncFactory.contexts.length).toEqual(1)
+      expect(vnode.asyncFactory.owners.length).toEqual(1)
     }
     }
     function loaded () {
     function loaded () {
+      setCurrentRenderingInstance(vm)
       vnode = createComponent(async, data, vm, vm)
       vnode = createComponent(async, data, vm, vm)
+      setCurrentRenderingInstance(null)
       expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
       expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
       expect(vnode.data.staticAttrs).toEqual({ class: 'foo' })
       expect(vnode.data.staticAttrs).toEqual({ class: 'foo' })
       expect(vnode.children).toBeUndefined()
       expect(vnode.children).toBeUndefined()
@@ -69,7 +74,7 @@ describe('create-component', () => {
       expect(vnode.elm).toBeUndefined()
       expect(vnode.elm).toBeUndefined()
       expect(vnode.ns).toBeUndefined()
       expect(vnode.ns).toBeUndefined()
       expect(vnode.context).toEqual(vm)
       expect(vnode.context).toEqual(vm)
-      expect(vnode.asyncFactory.contexts.length).toEqual(0)
+      expect(vnode.asyncFactory.owners.length).toEqual(0)
       expect(vm.$forceUpdate).toHaveBeenCalled()
       expect(vm.$forceUpdate).toHaveBeenCalled()
       done()
       done()
     }
     }
@@ -90,7 +95,7 @@ describe('create-component', () => {
     }
     }
     const vnode = createComponent(async, data, vm, vm)
     const vnode = createComponent(async, data, vm, vm)
     expect(vnode.asyncFactory).toBe(async)
     expect(vnode.asyncFactory).toBe(async)
-    expect(vnode.asyncFactory.contexts.length).toEqual(0)
+    expect(vnode.asyncFactory.owners.length).toEqual(0)
     expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
     expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
     expect(vnode.data.staticAttrs).toEqual({ class: 'bar' })
     expect(vnode.data.staticAttrs).toEqual({ class: 'bar' })
     expect(vnode.children).toBeUndefined()
     expect(vnode.children).toBeUndefined()