hmr.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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 } from '@vue/shared'
  12. export let isHmrUpdating = false
  13. export const hmrDirtyComponents = new Set<ConcreteComponent>()
  14. export interface HMRRuntime {
  15. createRecord: typeof createRecord
  16. rerender: typeof rerender
  17. reload: typeof reload
  18. }
  19. // Expose the HMR runtime on the global object
  20. // This makes it entirely tree-shakable without polluting the exports and makes
  21. // it easier to be used in toolings like vue-loader
  22. // Note: for a component to be eligible for HMR it also needs the __hmrId option
  23. // to be set so that its instances can be registered / removed.
  24. if (__DEV__ && (__BROWSER__ || __TEST__)) {
  25. const globalObject: any =
  26. typeof global !== 'undefined'
  27. ? global
  28. : typeof self !== 'undefined'
  29. ? self
  30. : typeof window !== 'undefined'
  31. ? window
  32. : {}
  33. globalObject.__VUE_HMR_RUNTIME__ = {
  34. createRecord: tryWrap(createRecord),
  35. rerender: tryWrap(rerender),
  36. reload: tryWrap(reload)
  37. } as HMRRuntime
  38. }
  39. type HMRRecord = Set<ComponentInternalInstance>
  40. const map: Map<string, HMRRecord> = new Map()
  41. export function registerHMR(instance: ComponentInternalInstance) {
  42. const id = instance.type.__hmrId!
  43. let record = map.get(id)
  44. if (!record) {
  45. createRecord(id)
  46. record = map.get(id)!
  47. }
  48. record.add(instance)
  49. }
  50. export function unregisterHMR(instance: ComponentInternalInstance) {
  51. map.get(instance.type.__hmrId!)!.delete(instance)
  52. }
  53. function createRecord(id: string): boolean {
  54. if (map.has(id)) {
  55. return false
  56. }
  57. map.set(id, new Set())
  58. return true
  59. }
  60. function rerender(id: string, newRender?: Function) {
  61. const record = map.get(id)
  62. if (!record) return
  63. // Array.from creates a snapshot which avoids the set being mutated during
  64. // updates
  65. Array.from(record).forEach(instance => {
  66. if (newRender) {
  67. instance.render = newRender as InternalRenderFunction
  68. }
  69. instance.renderCache = []
  70. // this flag forces child components with slot content to update
  71. isHmrUpdating = true
  72. instance.update()
  73. isHmrUpdating = false
  74. })
  75. }
  76. function reload(id: string, newComp: ComponentOptions | ClassComponent) {
  77. const record = map.get(id)
  78. if (!record) return
  79. // Array.from creates a snapshot which avoids the set being mutated during
  80. // updates
  81. Array.from(record).forEach(instance => {
  82. const comp = instance.type
  83. if (!hmrDirtyComponents.has(comp)) {
  84. // 1. Update existing comp definition to match new one
  85. newComp = isClassComponent(newComp) ? newComp.__vccOpts : newComp
  86. extend(comp, newComp)
  87. for (const key in comp) {
  88. if (!(key in newComp)) {
  89. delete (comp as any)[key]
  90. }
  91. }
  92. // 2. Mark component dirty. This forces the renderer to replace the component
  93. // on patch.
  94. hmrDirtyComponents.add(comp)
  95. // 3. Make sure to unmark the component after the reload.
  96. queuePostFlushCb(() => {
  97. hmrDirtyComponents.delete(comp)
  98. })
  99. }
  100. if (instance.parent) {
  101. // 4. Force the parent instance to re-render. This will cause all updated
  102. // components to be unmounted and re-mounted. Queue the update so that we
  103. // don't end up forcing the same parent to re-render multiple times.
  104. queueJob(instance.parent.update)
  105. } else if (instance.appContext.reload) {
  106. // root instance mounted via createApp() has a reload method
  107. instance.appContext.reload()
  108. } else if (typeof window !== 'undefined') {
  109. // root instance inside tree created via raw render(). Force reload.
  110. window.location.reload()
  111. } else {
  112. console.warn(
  113. '[HMR] Root or manually mounted instance modified. Full reload required.'
  114. )
  115. }
  116. })
  117. }
  118. function tryWrap(fn: (id: string, arg: any) => any): Function {
  119. return (id: string, arg: any) => {
  120. try {
  121. return fn(id, arg)
  122. } catch (e) {
  123. console.error(e)
  124. console.warn(
  125. `[HMR] Something went wrong during Vue component hot-reload. ` +
  126. `Full reload required.`
  127. )
  128. }
  129. }
  130. }