diff --git a/packages/compiler-sfc/src/parse.ts b/packages/compiler-sfc/src/parse.ts index edf27c07..16e67aa2 100644 --- a/packages/compiler-sfc/src/parse.ts +++ b/packages/compiler-sfc/src/parse.ts @@ -58,7 +58,7 @@ export function parse( sourceMap = true, filename = 'component.vue', sourceRoot = '', - pad = 'line' + pad = false }: SFCParseOptions = {} ): SFCDescriptor { const sourceKey = source + sourceMap + filename + sourceRoot + pad diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index dd35dbe9..2dea6eef 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -69,6 +69,7 @@ export interface ComponentOptionsBase< // SFC & dev only __scopeId?: string __hmrId?: string + __hmrUpdated?: boolean // type-only differentiator to separate OptionWithoutProps from a constructor // type returned by createComponent() or FunctionalComponent @@ -150,7 +151,6 @@ type ComponentInjectOptions = string | symbol | { from: string | symbol; default?: unknown } > -// TODO type inference for these options export interface LegacyOptions< Props, RawBindings, diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index be4ee168..d81155b3 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -37,7 +37,10 @@ export interface FunctionalComponent

{ props?: ComponentPropsOptions

inheritAttrs?: boolean displayName?: string + + // internal HMR related flags __hmrId?: string + __hmrUpdated?: boolean } export type Component = ComponentOptions | FunctionalComponent @@ -136,6 +139,9 @@ export interface ComponentInternalInstance { [LifecycleHooks.ACTIVATED]: LifecycleHook [LifecycleHooks.DEACTIVATED]: LifecycleHook [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook + + // hmr marker (dev only) + renderUpdated?: boolean } const emptyAppContext = createAppContext() diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 4d5799ca..01629fd2 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -111,10 +111,25 @@ export function renderComponentRoot( export function shouldUpdateComponent( prevVNode: VNode, nextVNode: VNode, + parentComponent: ComponentInternalInstance | null, optimized?: boolean ): boolean { const { props: prevProps, children: prevChildren } = prevVNode const { props: nextProps, children: nextChildren, patchFlag } = nextVNode + + // Parent component's render function was hot-updated. Since this may have + // caused the child component's slots content to have changed, we need to + // force the child to update as well. + if ( + __BUNDLER__ && + __DEV__ && + (prevChildren || nextChildren) && + parentComponent && + parentComponent.renderUpdated + ) { + return true + } + if (patchFlag > 0) { if (patchFlag & PatchFlags.DYNAMIC_SLOTS) { // slot content that references values that might have changed, diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 10ea1341..7c10defb 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -3,6 +3,7 @@ import { ComponentOptions, RenderFunction } from './component' +import { queueJob, queuePostFlushCb } from './scheduler' // Expose the HMR runtime on the global object // This makes it entirely tree-shakable without polluting the exports and makes @@ -20,7 +21,6 @@ if (__BUNDLER__ && __DEV__) { : {} globalObject.__VUE_HMR_RUNTIME__ = { - isRecorded: tryWrap(isRecorded), createRecord: tryWrap(createRecord), rerender: tryWrap(rerender), reload: tryWrap(reload) @@ -42,42 +42,69 @@ export function unregisterHMR(instance: ComponentInternalInstance) { map.get(instance.type.__hmrId!)!.instances.delete(instance) } -function isRecorded(id: string): boolean { - return map.has(id) -} - -function createRecord(id: string, comp: ComponentOptions) { +function createRecord(id: string, comp: ComponentOptions): boolean { if (map.has(id)) { - return + return false } map.set(id, { comp, instances: new Set() }) + return true } function rerender(id: string, newRender: RenderFunction) { map.get(id)!.instances.forEach(instance => { instance.render = newRender instance.renderCache = [] + // this flag forces child components with slot content to update + instance.renderUpdated = true instance.update() - // TODO force scoped slots passed to children to have DYNAMIC_SLOTS flag + instance.renderUpdated = false }) } function reload(id: string, newComp: ComponentOptions) { - // TODO - console.log('reload', id) + const record = map.get(id)! + // 1. Update existing comp definition to match new one + const comp = record.comp + Object.assign(comp, newComp) + for (const key in comp) { + if (!(key in newComp)) { + delete (comp as any)[key] + } + } + // 2. Mark component dirty. This forces the renderer to replace the component + // on patch. + comp.__hmrUpdated = true + 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 (typeof window !== 'undefined') { + window.location.reload() + } else { + console.warn( + '[HMR] Root or manually mounted instance modified. Full reload required.' + ) + } + }) + // 4. Make sure to unmark the component after the reload. + queuePostFlushCb(() => { + comp.__hmrUpdated = false + }) } -function tryWrap(fn: (id: string, arg: any) => void): Function { +function tryWrap(fn: (id: string, arg: any) => any): Function { return (id: string, arg: any) => { try { - fn(id, arg) + return fn(id, arg) } catch (e) { console.error(e) console.warn( - `Something went wrong during Vue component hot-reload. ` + + `[HMR] Something went wrong during Vue component hot-reload. ` + `Full reload required.` ) } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d3c38c3d..3a27390e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -804,7 +804,7 @@ export function createRenderer< } else { const instance = (n2.component = n1.component)! - if (shouldUpdateComponent(n1, n2, optimized)) { + if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) { if ( __FEATURE_SUSPENSE__ && instance.asyncDep && diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index d47bf78c..89f2b48c 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -185,6 +185,15 @@ export function isVNode(value: any): value is VNode { } export function isSameVNodeType(n1: VNode, n2: VNode): boolean { + if ( + __BUNDLER__ && + __DEV__ && + n2.shapeFlag & ShapeFlags.COMPONENT && + (n2.type as Component).__hmrUpdated + ) { + // HMR only: if the component has been hot-updated, force a reload. + return false + } return n1.type === n2.type && n1.key === n2.key }