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>) {
   // install runtime convenience helpers
   installRenderHelpers(Vue.prototype)
@@ -76,6 +83,10 @@ export function renderMixin (Vue: Class<Component>) {
     // render self
     let vnode
     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)
     } catch (e) {
       handleError(e, vm, `render`)
@@ -92,6 +103,8 @@ export function renderMixin (Vue: Class<Component>) {
       } else {
         vnode = vm._vnode
       }
+    } finally {
+      currentRenderingInstance = null
     }
     // if the returned array contains only a single node, allow it
     if (Array.isArray(vnode) && vnode.length === 1) {

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

@@ -129,7 +129,7 @@ export function createComponent (
   let asyncFactory
   if (isUndef(Ctor.cid)) {
     asyncFactory = Ctor
-    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
+    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
     if (Ctor === undefined) {
       // return a placeholder node for async component, which is rendered
       // 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'
 
 import { createEmptyVNode } from 'core/vdom/vnode'
+import { currentRenderingInstance } from 'core/instance/render'
 
 function ensureCtor (comp: any, base) {
   if (
@@ -40,8 +41,7 @@ export function createAsyncPlaceholder (
 
 export function resolveAsyncComponent (
   factory: Function,
-  baseCtor: Class<Component>,
-  context: Component
+  baseCtor: Class<Component>
 ): Class<Component> | void {
   if (isTrue(factory.error) && isDef(factory.errorComp)) {
     return factory.errorComp
@@ -55,20 +55,21 @@ export function resolveAsyncComponent (
     return factory.loadingComp
   }
 
-  if (isDef(factory.contexts)) {
+  const owner = currentRenderingInstance
+  if (isDef(factory.owners)) {
     // already pending
-    factory.contexts.push(context)
+    factory.owners.push(owner)
   } else {
-    const contexts = factory.contexts = [context]
+    const owners = factory.owners = [owner]
     let sync = true
 
     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) {
-        contexts.length = 0
+        owners.length = 0
       }
     }
 
@@ -80,7 +81,7 @@ export function resolveAsyncComponent (
       if (!sync) {
         forceRender(true)
       } 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)
     }).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 { createComponent } from 'core/vdom/create-component'
+import { setCurrentRenderingInstance } from 'core/instance/render'
 
 describe('create-component', () => {
   let vm
@@ -55,13 +56,17 @@ describe('create-component', () => {
       }, 0)
     }
     function go () {
+      setCurrentRenderingInstance(vm)
       vnode = createComponent(async, data, vm, vm)
+      setCurrentRenderingInstance(null)
       expect(vnode.isComment).toBe(true) // not to be loaded yet.
       expect(vnode.asyncFactory).toBe(async)
-      expect(vnode.asyncFactory.contexts.length).toEqual(1)
+      expect(vnode.asyncFactory.owners.length).toEqual(1)
     }
     function loaded () {
+      setCurrentRenderingInstance(vm)
       vnode = createComponent(async, data, vm, vm)
+      setCurrentRenderingInstance(null)
       expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
       expect(vnode.data.staticAttrs).toEqual({ class: 'foo' })
       expect(vnode.children).toBeUndefined()
@@ -69,7 +74,7 @@ describe('create-component', () => {
       expect(vnode.elm).toBeUndefined()
       expect(vnode.ns).toBeUndefined()
       expect(vnode.context).toEqual(vm)
-      expect(vnode.asyncFactory.contexts.length).toEqual(0)
+      expect(vnode.asyncFactory.owners.length).toEqual(0)
       expect(vm.$forceUpdate).toHaveBeenCalled()
       done()
     }
@@ -90,7 +95,7 @@ describe('create-component', () => {
     }
     const vnode = createComponent(async, data, vm, vm)
     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.data.staticAttrs).toEqual({ class: 'bar' })
     expect(vnode.children).toBeUndefined()