Skip to content

Commit

Permalink
fix: async component should use render owner as force update context
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yyx990803 committed Feb 6, 2019
1 parent 2ef67f8 commit b9de23b
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 13 deletions.
13 changes: 13 additions & 0 deletions src/core/instance/render.js
Expand Up @@ -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)
Expand All @@ -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`)
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/vdom/create-component.js
Expand Up @@ -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.
Expand Down
19 changes: 10 additions & 9 deletions src/core/vdom/helpers/resolve-async-component.js
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}

Expand All @@ -80,7 +81,7 @@ export function resolveAsyncComponent (
if (!sync) {
forceRender(true)
} else {
contexts.length = 0
owners.length = 0
}
})

Expand Down
34 changes: 34 additions & 0 deletions test/unit/features/component/component-scoped-slot.spec.js
Expand Up @@ -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)
}
})
})
11 changes: 8 additions & 3 deletions 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
Expand Down Expand Up @@ -55,21 +56,25 @@ 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()
expect(vnode.text).toBeUndefined()
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()
}
Expand All @@ -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()
Expand Down

0 comments on commit b9de23b

Please sign in to comment.