Skip to content

Commit b9de23b

Browse files
committed
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
1 parent 2ef67f8 commit b9de23b

File tree

5 files changed

+66
-13
lines changed

5 files changed

+66
-13
lines changed

src/core/instance/render.js

+13
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ export function initRender (vm: Component) {
5151
}
5252
}
5353

54+
export let currentRenderingInstance: Component | null = null
55+
56+
// for testing only
57+
export function setCurrentRenderingInstance (vm: Component) {
58+
currentRenderingInstance = vm
59+
}
60+
5461
export function renderMixin (Vue: Class<Component>) {
5562
// install runtime convenience helpers
5663
installRenderHelpers(Vue.prototype)
@@ -76,6 +83,10 @@ export function renderMixin (Vue: Class<Component>) {
7683
// render self
7784
let vnode
7885
try {
86+
// There's no need to maintain a stack becaues all render fns are called
87+
// separately from one another. Nested component's render fns are called
88+
// when parent component is patched.
89+
currentRenderingInstance = vm
7990
vnode = render.call(vm._renderProxy, vm.$createElement)
8091
} catch (e) {
8192
handleError(e, vm, `render`)
@@ -92,6 +103,8 @@ export function renderMixin (Vue: Class<Component>) {
92103
} else {
93104
vnode = vm._vnode
94105
}
106+
} finally {
107+
currentRenderingInstance = null
95108
}
96109
// if the returned array contains only a single node, allow it
97110
if (Array.isArray(vnode) && vnode.length === 1) {

src/core/vdom/create-component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export function createComponent (
129129
let asyncFactory
130130
if (isUndef(Ctor.cid)) {
131131
asyncFactory = Ctor
132-
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
132+
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
133133
if (Ctor === undefined) {
134134
// return a placeholder node for async component, which is rendered
135135
// as a comment node but preserves all the raw information for the node.

src/core/vdom/helpers/resolve-async-component.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from 'core/util/index'
1313

1414
import { createEmptyVNode } from 'core/vdom/vnode'
15+
import { currentRenderingInstance } from 'core/instance/render'
1516

1617
function ensureCtor (comp: any, base) {
1718
if (
@@ -40,8 +41,7 @@ export function createAsyncPlaceholder (
4041

4142
export function resolveAsyncComponent (
4243
factory: Function,
43-
baseCtor: Class<Component>,
44-
context: Component
44+
baseCtor: Class<Component>
4545
): Class<Component> | void {
4646
if (isTrue(factory.error) && isDef(factory.errorComp)) {
4747
return factory.errorComp
@@ -55,20 +55,21 @@ export function resolveAsyncComponent (
5555
return factory.loadingComp
5656
}
5757

58-
if (isDef(factory.contexts)) {
58+
const owner = currentRenderingInstance
59+
if (isDef(factory.owners)) {
5960
// already pending
60-
factory.contexts.push(context)
61+
factory.owners.push(owner)
6162
} else {
62-
const contexts = factory.contexts = [context]
63+
const owners = factory.owners = [owner]
6364
let sync = true
6465

6566
const forceRender = (renderCompleted: boolean) => {
66-
for (let i = 0, l = contexts.length; i < l; i++) {
67-
contexts[i].$forceUpdate()
67+
for (let i = 0, l = owners.length; i < l; i++) {
68+
(owners[i]: any).$forceUpdate()
6869
}
6970

7071
if (renderCompleted) {
71-
contexts.length = 0
72+
owners.length = 0
7273
}
7374
}
7475

@@ -80,7 +81,7 @@ export function resolveAsyncComponent (
8081
if (!sync) {
8182
forceRender(true)
8283
} else {
83-
contexts.length = 0
84+
owners.length = 0
8485
}
8586
})
8687

test/unit/features/component/component-scoped-slot.spec.js

+34
Original file line numberDiff line numberDiff line change
@@ -935,4 +935,38 @@ describe('Component scoped slot', () => {
935935
expect(childUpdate.calls.count()).toBe(1)
936936
}).then(done)
937937
})
938+
939+
// #9432: async components inside a scoped slot should trigger update of the
940+
// component that invoked the scoped slot, not the lexical context component.
941+
it('async component inside scoped slot', done => {
942+
let p
943+
const vm = new Vue({
944+
template: `
945+
<foo>
946+
<template #default>
947+
<bar />
948+
</template>
949+
</foo>
950+
`,
951+
components: {
952+
foo: {
953+
template: `<div>foo<slot/></div>`
954+
},
955+
bar: resolve => {
956+
setTimeout(() => {
957+
resolve({
958+
template: `<div>bar</div>`
959+
})
960+
next()
961+
}, 0)
962+
}
963+
}
964+
}).$mount()
965+
966+
function next () {
967+
waitForUpdate(() => {
968+
expect(vm.$el.textContent).toBe(`foobar`)
969+
}).then(done)
970+
}
971+
})
938972
})

test/unit/modules/vdom/create-component.spec.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Vue from 'vue'
22
import { createComponent } from 'core/vdom/create-component'
3+
import { setCurrentRenderingInstance } from 'core/instance/render'
34

45
describe('create-component', () => {
56
let vm
@@ -55,21 +56,25 @@ describe('create-component', () => {
5556
}, 0)
5657
}
5758
function go () {
59+
setCurrentRenderingInstance(vm)
5860
vnode = createComponent(async, data, vm, vm)
61+
setCurrentRenderingInstance(null)
5962
expect(vnode.isComment).toBe(true) // not to be loaded yet.
6063
expect(vnode.asyncFactory).toBe(async)
61-
expect(vnode.asyncFactory.contexts.length).toEqual(1)
64+
expect(vnode.asyncFactory.owners.length).toEqual(1)
6265
}
6366
function loaded () {
67+
setCurrentRenderingInstance(vm)
6468
vnode = createComponent(async, data, vm, vm)
69+
setCurrentRenderingInstance(null)
6570
expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
6671
expect(vnode.data.staticAttrs).toEqual({ class: 'foo' })
6772
expect(vnode.children).toBeUndefined()
6873
expect(vnode.text).toBeUndefined()
6974
expect(vnode.elm).toBeUndefined()
7075
expect(vnode.ns).toBeUndefined()
7176
expect(vnode.context).toEqual(vm)
72-
expect(vnode.asyncFactory.contexts.length).toEqual(0)
77+
expect(vnode.asyncFactory.owners.length).toEqual(0)
7378
expect(vm.$forceUpdate).toHaveBeenCalled()
7479
done()
7580
}
@@ -90,7 +95,7 @@ describe('create-component', () => {
9095
}
9196
const vnode = createComponent(async, data, vm, vm)
9297
expect(vnode.asyncFactory).toBe(async)
93-
expect(vnode.asyncFactory.contexts.length).toEqual(0)
98+
expect(vnode.asyncFactory.owners.length).toEqual(0)
9499
expect(vnode.tag).toMatch(/vue-component-[0-9]+-child/)
95100
expect(vnode.data.staticAttrs).toEqual({ class: 'bar' })
96101
expect(vnode.children).toBeUndefined()

0 commit comments

Comments
 (0)