hmr.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /* eslint-disable no-restricted-globals */
  2. import {
  3. ConcreteComponent,
  4. ComponentInternalInstance,
  5. ComponentOptions,
  6. InternalRenderFunction,
  7. ClassComponent,
  8. isClassComponent
  9. } from './component'
  10. import { queueJob, queuePostFlushCb } from './scheduler'
  11. import { extend, getGlobalThis } from '@vue/shared'
  12. type HMRComponent = ComponentOptions | ClassComponent
  13. export let isHmrUpdating = false
  14. export const hmrDirtyComponents = new Set<ConcreteComponent>()
  15. export interface HMRRuntime {
  16. createRecord: typeof createRecord
  17. rerender: typeof rerender
  18. reload: typeof reload
  19. }
  20. // Expose the HMR runtime on the global object
  21. // This makes it entirely tree-shakable without polluting the exports and makes
  22. // it easier to be used in toolings like vue-loader
  23. // Note: for a component to be eligible for HMR it also needs the __hmrId option
  24. // to be set so that its instances can be registered / removed.
  25. if (__DEV__) {
  26. getGlobalThis().__VUE_HMR_RUNTIME__ = {
  27. createRecord: tryWrap(createRecord),
  28. rerender: tryWrap(rerender),
  29. reload: tryWrap(reload)
  30. } as HMRRuntime
  31. }
  32. const map: Map<
  33. string,
  34. {
  35. // the initial component definition is recorded on import - this allows us
  36. // to apply hot updates to the component even when there are no actively
  37. // rendered instance.
  38. initialDef: ComponentOptions
  39. instances: Set<ComponentInternalInstance>
  40. }
  41. > = new Map()
  42. export function registerHMR(instance: ComponentInternalInstance) {
  43. const id = instance.type.__hmrId!
  44. let record = map.get(id)
  45. if (!record) {
  46. createRecord(id, instance.type as HMRComponent)
  47. record = map.get(id)!
  48. }
  49. record.instances.add(instance)
  50. }
  51. export function unregisterHMR(instance: ComponentInternalInstance) {
  52. map.get(instance.type.__hmrId!)!.instances.delete(instance)
  53. }
  54. function createRecord(id: string, initialDef: HMRComponent): boolean {
  55. if (map.has(id)) {
  56. return false
  57. }
  58. map.set(id, {
  59. initialDef: normalizeClassComponent(initialDef),
  60. instances: new Set()
  61. })
  62. return true
  63. }
  64. function normalizeClassComponent(component: HMRComponent): ComponentOptions {
  65. return isClassComponent(component) ? component.__vccOpts : component
  66. }
  67. function rerender(id: string, newRender?: Function) {
  68. const record = map.get(id)
  69. if (!record) {
  70. return
  71. }
  72. // update initial record (for not-yet-rendered component)
  73. record.initialDef.render = newRender
  74. // Create a snapshot which avoids the set being mutated during updates
  75. ;[...record.instances].forEach(instance => {
  76. if (newRender) {
  77. instance.render = newRender as InternalRenderFunction
  78. normalizeClassComponent(instance.type as HMRComponent).render = newRender
  79. }
  80. instance.renderCache = []
  81. // this flag forces child components with slot content to update
  82. isHmrUpdating = true
  83. instance.update()
  84. isHmrUpdating = false
  85. })
  86. }
  87. function reload(id: string, newComp: HMRComponent) {
  88. const record = map.get(id)
  89. if (!record) return
  90. newComp = normalizeClassComponent(newComp)
  91. // update initial def (for not-yet-rendered components)
  92. updateComponentDef(record.initialDef, newComp)
  93. // create a snapshot which avoids the set being mutated during updates
  94. const instances = [...record.instances]
  95. for (const instance of instances) {
  96. const oldComp = normalizeClassComponent(instance.type as HMRComponent)
  97. if (!hmrDirtyComponents.has(oldComp)) {
  98. // 1. Update existing comp definition to match new one
  99. if (oldComp !== record.initialDef) {
  100. updateComponentDef(oldComp, newComp)
  101. }
  102. // 2. mark definition dirty. This forces the renderer to replace the
  103. // component on patch.
  104. hmrDirtyComponents.add(oldComp)
  105. }
  106. // 3. invalidate options resolution cache
  107. instance.appContext.optionsCache.delete(instance.type as any)
  108. // 4. actually update
  109. if (instance.ceReload) {
  110. // custom element
  111. hmrDirtyComponents.add(oldComp)
  112. instance.ceReload((newComp as any).styles)
  113. hmrDirtyComponents.delete(oldComp)
  114. } else if (instance.parent) {
  115. // 4. Force the parent instance to re-render. This will cause all updated
  116. // components to be unmounted and re-mounted. Queue the update so that we
  117. // don't end up forcing the same parent to re-render multiple times.
  118. queueJob(instance.parent.update)
  119. } else if (instance.appContext.reload) {
  120. // root instance mounted via createApp() has a reload method
  121. instance.appContext.reload()
  122. } else if (typeof window !== 'undefined') {
  123. // root instance inside tree created via raw render(). Force reload.
  124. window.location.reload()
  125. } else {
  126. console.warn(
  127. '[HMR] Root or manually mounted instance modified. Full reload required.'
  128. )
  129. }
  130. }
  131. // 5. make sure to cleanup dirty hmr components after update
  132. queuePostFlushCb(() => {
  133. for (const instance of instances) {
  134. hmrDirtyComponents.delete(
  135. normalizeClassComponent(instance.type as HMRComponent)
  136. )
  137. }
  138. })
  139. }
  140. function updateComponentDef(
  141. oldComp: ComponentOptions,
  142. newComp: ComponentOptions
  143. ) {
  144. extend(oldComp, newComp)
  145. for (const key in oldComp) {
  146. if (key !== '__file' && !(key in newComp)) {
  147. delete (oldComp as any)[key]
  148. }
  149. }
  150. }
  151. function tryWrap(fn: (id: string, arg: any) => any): Function {
  152. return (id: string, arg: any) => {
  153. try {
  154. return fn(id, arg)
  155. } catch (e: any) {
  156. console.error(e)
  157. console.warn(
  158. `[HMR] Something went wrong during Vue component hot-reload. ` +
  159. `Full reload required.`
  160. )
  161. }
  162. }
  163. }