hmr.ts 6.4 KB

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