diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index defe9af8cd3..57d77332a3f 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -6,7 +6,7 @@ import { RootRenderFunction } from './renderer' import { InjectionKey } from './apiInject' import { isFunction, NO, isObject } from '@vue/shared' import { warn } from './warning' -import { createVNode } from './vnode' +import { createVNode, cloneVNode } from './vnode' export interface App { config: AppConfig @@ -47,6 +47,7 @@ export interface AppContext { components: Record directives: Record provides: Record + reload?: () => void // HMR only } type PluginInstallFunction = (app: App) => any @@ -175,6 +176,14 @@ export function createAppAPI( // store app context on the root VNode. // this will be set on the root instance on initial mount. vnode.appContext = context + + // HMR root reload + if (__BUNDLER__ && __DEV__) { + context.reload = () => { + render(cloneVNode(vnode), rootContainer) + } + } + render(vnode, rootContainer) isMounted = true return vnode.component!.proxy diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 84433067e01..cdbf91bea77 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -60,7 +60,9 @@ function createRecord(id: string, comp: ComponentOptions): boolean { } function rerender(id: string, newRender?: RenderFunction) { - map.get(id)!.instances.forEach(instance => { + // Array.from creates a snapshot which avoids the set being mutated during + // updates + Array.from(map.get(id)!.instances).forEach(instance => { if (newRender) { instance.render = newRender } @@ -85,13 +87,19 @@ function reload(id: string, newComp: ComponentOptions) { // 2. Mark component dirty. This forces the renderer to replace the component // on patch. comp.__hmrUpdated = true - record.instances.forEach(instance => { + // Array.from creates a snapshot which avoids the set being mutated during + // updates + Array.from(record.instances).forEach(instance => { if (instance.parent) { // 3. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. queueJob(instance.parent.update) + } else if (instance.appContext.reload) { + // root instance mounted via createApp() has a reload method + instance.appContext.reload() } else if (typeof window !== 'undefined') { + // root instance inside tree created via raw render(). Force reload. window.location.reload() } else { console.warn( diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index c651247ccd2..f81e803ca1c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1727,12 +1727,12 @@ export function createRenderer< } } - const render: RootRenderFunction< - HostNode, - HostElement & { - _vnode: HostVNode | null - } - > = (vnode, container) => { + type HostRootElement = HostElement & { _vnode: HostVNode | null } + + const render: RootRenderFunction = ( + vnode, + container: HostRootElement + ) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 05cbcf54103..ac4e6e236e3 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -15,7 +15,7 @@ import { import { RawSlots } from './componentSlots' import { ShapeFlags } from './shapeFlags' import { isReactive, Ref } from '@vue/reactivity' -import { AppContext } from './apiApp' +import { AppContext } from './apiCreateApp' import { SuspenseBoundary } from './components/Suspense' import { DirectiveBinding } from './directives' import { SuspenseImpl } from './components/Suspense'