From b9de23b1008b52deca7e7df40843e318a42f3f53 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Feb 2019 22:16:55 -0500 Subject: [PATCH] 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 --- src/core/instance/render.js | 13 +++++++ src/core/vdom/create-component.js | 2 +- .../vdom/helpers/resolve-async-component.js | 19 ++++++----- .../component/component-scoped-slot.spec.js | 34 +++++++++++++++++++ .../modules/vdom/create-component.spec.js | 11 ++++-- 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/core/instance/render.js b/src/core/instance/render.js index 2b9c8d11951..f3ebe8d7a96 100644 --- a/src/core/instance/render.js +++ b/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) { // install runtime convenience helpers installRenderHelpers(Vue.prototype) @@ -76,6 +83,10 @@ export function renderMixin (Vue: Class) { // 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) { } 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) { diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 658b7ed8a7f..1e9bb217a6a 100644 --- a/src/core/vdom/create-component.js +++ b/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. diff --git a/src/core/vdom/helpers/resolve-async-component.js b/src/core/vdom/helpers/resolve-async-component.js index 52ecd2c925c..5714b4ead00 100644 --- a/src/core/vdom/helpers/resolve-async-component.js +++ b/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, - context: Component + baseCtor: Class ): Class | 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 } }) diff --git a/test/unit/features/component/component-scoped-slot.spec.js b/test/unit/features/component/component-scoped-slot.spec.js index 5141d9b1205..9a2dd0cc714 100644 --- a/test/unit/features/component/component-scoped-slot.spec.js +++ b/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: ` + + + + `, + components: { + foo: { + template: `
foo
` + }, + bar: resolve => { + setTimeout(() => { + resolve({ + template: `
bar
` + }) + next() + }, 0) + } + } + }).$mount() + + function next () { + waitForUpdate(() => { + expect(vm.$el.textContent).toBe(`foobar`) + }).then(done) + } + }) }) diff --git a/test/unit/modules/vdom/create-component.spec.js b/test/unit/modules/vdom/create-component.spec.js index c5a1f717c95..9ca34f68280 100644 --- a/test/unit/modules/vdom/create-component.spec.js +++ b/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()