hmr.ts 3.7 KB

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