]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(hmr): reload and force slot update on re-render
authorEvan You <yyx990803@gmail.com>
Thu, 12 Dec 2019 23:13:59 +0000 (18:13 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 13 Dec 2019 02:09:47 +0000 (21:09 -0500)
packages/compiler-sfc/src/parse.ts
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts

index edf27c075d9002ef78da21dc951218948186b5e3..16e67aa2705d2ba5540506c044e814d78a8bc017 100644 (file)
@@ -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
index dd35dbe9708dc6a5803fc03ec09d865eb6fc024a..2dea6eef4f16e82dd981cc7ec9bed0b88b2cd682 100644 (file)
@@ -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,
index be4ee1685964d3ca76923345cf0328747e97acee..d81155b3958f1c2ceb482ca99ffd49e17d818c54 100644 (file)
@@ -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()
index 4d5799ca0c4c43cca8a5e7bfcdef576c13b374df..01629fd2d1a0d0bad9c5b87ec304a66928d06102 100644 (file)
@@ -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,
index 10ea1341127d705a587125dfe3a76d6d4c75a153..7c10defbab09330a5e12e35ce9253b57354da142 100644 (file)
@@ -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.`
       )
     }
index d3c38c3dc3882c7660038fea70719446d5b5fbb0..3a27390e106d94f3fe3d370b58e60c05dcd039c7 100644 (file)
@@ -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 &&
index d47bf78c8ceccd8c2aead1250ffd871a4ac82216..89f2b48c01fb9afecf1aaf90bbba6ef9ffd640fa 100644 (file)
@@ -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
 }