feat(hmr): reload and force slot update on re-render
This commit is contained in:
parent
ef50c333ce
commit
f77ae132e5
@ -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
|
||||
|
@ -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,
|
||||
|
@ -37,7 +37,10 @@ export interface FunctionalComponent<P = {}> {
|
||||
props?: ComponentPropsOptions<P>
|
||||
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()
|
||||
|
@ -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,
|
||||
|
@ -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.`
|
||||
)
|
||||
}
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user