feat(hmr): reload and force slot update on re-render

This commit is contained in:
Evan You 2019-12-12 18:13:59 -05:00
parent ef50c333ce
commit f77ae132e5
7 changed files with 73 additions and 16 deletions

View File

@ -58,7 +58,7 @@ export function parse(
sourceMap = true, sourceMap = true,
filename = 'component.vue', filename = 'component.vue',
sourceRoot = '', sourceRoot = '',
pad = 'line' pad = false
}: SFCParseOptions = {} }: SFCParseOptions = {}
): SFCDescriptor { ): SFCDescriptor {
const sourceKey = source + sourceMap + filename + sourceRoot + pad const sourceKey = source + sourceMap + filename + sourceRoot + pad

View File

@ -69,6 +69,7 @@ export interface ComponentOptionsBase<
// SFC & dev only // SFC & dev only
__scopeId?: string __scopeId?: string
__hmrId?: string __hmrId?: string
__hmrUpdated?: boolean
// type-only differentiator to separate OptionWithoutProps from a constructor // type-only differentiator to separate OptionWithoutProps from a constructor
// type returned by createComponent() or FunctionalComponent // type returned by createComponent() or FunctionalComponent
@ -150,7 +151,6 @@ type ComponentInjectOptions =
string | symbol | { from: string | symbol; default?: unknown } string | symbol | { from: string | symbol; default?: unknown }
> >
// TODO type inference for these options
export interface LegacyOptions< export interface LegacyOptions<
Props, Props,
RawBindings, RawBindings,

View File

@ -37,7 +37,10 @@ export interface FunctionalComponent<P = {}> {
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
inheritAttrs?: boolean inheritAttrs?: boolean
displayName?: string displayName?: string
// internal HMR related flags
__hmrId?: string __hmrId?: string
__hmrUpdated?: boolean
} }
export type Component = ComponentOptions | FunctionalComponent export type Component = ComponentOptions | FunctionalComponent
@ -136,6 +139,9 @@ export interface ComponentInternalInstance {
[LifecycleHooks.ACTIVATED]: LifecycleHook [LifecycleHooks.ACTIVATED]: LifecycleHook
[LifecycleHooks.DEACTIVATED]: LifecycleHook [LifecycleHooks.DEACTIVATED]: LifecycleHook
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
// hmr marker (dev only)
renderUpdated?: boolean
} }
const emptyAppContext = createAppContext() const emptyAppContext = createAppContext()

View File

@ -111,10 +111,25 @@ export function renderComponentRoot(
export function shouldUpdateComponent( export function shouldUpdateComponent(
prevVNode: VNode, prevVNode: VNode,
nextVNode: VNode, nextVNode: VNode,
parentComponent: ComponentInternalInstance | null,
optimized?: boolean optimized?: boolean
): boolean { ): boolean {
const { props: prevProps, children: prevChildren } = prevVNode const { props: prevProps, children: prevChildren } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode 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 > 0) {
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) { if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
// slot content that references values that might have changed, // slot content that references values that might have changed,

View File

@ -3,6 +3,7 @@ import {
ComponentOptions, ComponentOptions,
RenderFunction RenderFunction
} from './component' } from './component'
import { queueJob, queuePostFlushCb } from './scheduler'
// Expose the HMR runtime on the global object // Expose the HMR runtime on the global object
// This makes it entirely tree-shakable without polluting the exports and makes // This makes it entirely tree-shakable without polluting the exports and makes
@ -20,7 +21,6 @@ if (__BUNDLER__ && __DEV__) {
: {} : {}
globalObject.__VUE_HMR_RUNTIME__ = { globalObject.__VUE_HMR_RUNTIME__ = {
isRecorded: tryWrap(isRecorded),
createRecord: tryWrap(createRecord), createRecord: tryWrap(createRecord),
rerender: tryWrap(rerender), rerender: tryWrap(rerender),
reload: tryWrap(reload) reload: tryWrap(reload)
@ -42,42 +42,69 @@ export function unregisterHMR(instance: ComponentInternalInstance) {
map.get(instance.type.__hmrId!)!.instances.delete(instance) map.get(instance.type.__hmrId!)!.instances.delete(instance)
} }
function isRecorded(id: string): boolean { function createRecord(id: string, comp: ComponentOptions): boolean {
return map.has(id)
}
function createRecord(id: string, comp: ComponentOptions) {
if (map.has(id)) { if (map.has(id)) {
return return false
} }
map.set(id, { map.set(id, {
comp, comp,
instances: new Set() instances: new Set()
}) })
return true
} }
function rerender(id: string, newRender: RenderFunction) { function rerender(id: string, newRender: RenderFunction) {
map.get(id)!.instances.forEach(instance => { map.get(id)!.instances.forEach(instance => {
instance.render = newRender instance.render = newRender
instance.renderCache = [] instance.renderCache = []
// this flag forces child components with slot content to update
instance.renderUpdated = true
instance.update() instance.update()
// TODO force scoped slots passed to children to have DYNAMIC_SLOTS flag instance.renderUpdated = false
}) })
} }
function reload(id: string, newComp: ComponentOptions) { function reload(id: string, newComp: ComponentOptions) {
// TODO const record = map.get(id)!
console.log('reload', 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) => { return (id: string, arg: any) => {
try { try {
fn(id, arg) return fn(id, arg)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
console.warn( console.warn(
`Something went wrong during Vue component hot-reload. ` + `[HMR] Something went wrong during Vue component hot-reload. ` +
`Full reload required.` `Full reload required.`
) )
} }

View File

@ -804,7 +804,7 @@ export function createRenderer<
} else { } else {
const instance = (n2.component = n1.component)! const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) { if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
if ( if (
__FEATURE_SUSPENSE__ && __FEATURE_SUSPENSE__ &&
instance.asyncDep && instance.asyncDep &&

View File

@ -185,6 +185,15 @@ export function isVNode(value: any): value is VNode {
} }
export function isSameVNodeType(n1: VNode, n2: VNode): boolean { 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 return n1.type === n2.type && n1.key === n2.key
} }