hmr.ts 4.0 KB

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