hydrationStrategies.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { getGlobalThis, isString } from '@vue/shared'
  2. import { DOMNodeTypes, isComment } from './hydration'
  3. // Polyfills for Safari support
  4. // see https://caniuse.com/requestidlecallback
  5. const requestIdleCallback: Window['requestIdleCallback'] =
  6. getGlobalThis().requestIdleCallback || (cb => setTimeout(cb, 1))
  7. const cancelIdleCallback: Window['cancelIdleCallback'] =
  8. getGlobalThis().cancelIdleCallback || (id => clearTimeout(id))
  9. /**
  10. * A lazy hydration strategy for async components.
  11. * @param hydrate - call this to perform the actual hydration.
  12. * @param forEachElement - iterate through the root elements of the component's
  13. * non-hydrated DOM, accounting for possible fragments.
  14. * @returns a teardown function to be called if the async component is unmounted
  15. * before it is hydrated. This can be used to e.g. remove DOM event
  16. * listeners.
  17. */
  18. export type HydrationStrategy = (
  19. hydrate: () => void,
  20. forEachElement: (cb: (el: Element) => any) => void,
  21. ) => (() => void) | void
  22. export type HydrationStrategyFactory<Options> = (
  23. options?: Options,
  24. ) => HydrationStrategy
  25. export const hydrateOnIdle: HydrationStrategyFactory<number> =
  26. (timeout = 10000) =>
  27. hydrate => {
  28. const id = requestIdleCallback(hydrate, { timeout })
  29. return () => cancelIdleCallback(id)
  30. }
  31. function elementIsVisibleInViewport(el: Element) {
  32. const { top, left, bottom, right } = el.getBoundingClientRect()
  33. // eslint-disable-next-line no-restricted-globals
  34. const { innerHeight, innerWidth } = window
  35. return (
  36. ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) &&
  37. ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
  38. )
  39. }
  40. export const hydrateOnVisible: HydrationStrategyFactory<
  41. IntersectionObserverInit
  42. > = opts => (hydrate, forEach) => {
  43. const ob = new IntersectionObserver(entries => {
  44. for (const e of entries) {
  45. if (!e.isIntersecting) continue
  46. ob.disconnect()
  47. hydrate()
  48. break
  49. }
  50. }, opts)
  51. forEach(el => {
  52. if (!(el instanceof Element)) return
  53. if (elementIsVisibleInViewport(el)) {
  54. hydrate()
  55. ob.disconnect()
  56. return false
  57. }
  58. ob.observe(el)
  59. })
  60. return () => ob.disconnect()
  61. }
  62. export const hydrateOnMediaQuery: HydrationStrategyFactory<string> =
  63. query => hydrate => {
  64. if (query) {
  65. const mql = matchMedia(query)
  66. if (mql.matches) {
  67. hydrate()
  68. } else {
  69. mql.addEventListener('change', hydrate, { once: true })
  70. return () => mql.removeEventListener('change', hydrate)
  71. }
  72. }
  73. }
  74. export const hydrateOnInteraction: HydrationStrategyFactory<
  75. keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>
  76. > =
  77. (interactions = []) =>
  78. (hydrate, forEach) => {
  79. if (isString(interactions)) interactions = [interactions]
  80. let hasHydrated = false
  81. const doHydrate = (e: Event) => {
  82. if (!hasHydrated) {
  83. hasHydrated = true
  84. teardown()
  85. hydrate()
  86. // replay event
  87. e.target!.dispatchEvent(new (e.constructor as any)(e.type, e))
  88. }
  89. }
  90. const teardown = () => {
  91. forEach(el => {
  92. for (const i of interactions) {
  93. el.removeEventListener(i, doHydrate)
  94. }
  95. })
  96. }
  97. forEach(el => {
  98. for (const i of interactions) {
  99. el.addEventListener(i, doHydrate, { once: true })
  100. }
  101. })
  102. return teardown
  103. }
  104. export function forEachElement(
  105. node: Node,
  106. cb: (el: Element) => void | false,
  107. ): void {
  108. // fragment
  109. if (isComment(node) && node.data === '[') {
  110. let depth = 1
  111. let next = node.nextSibling
  112. while (next) {
  113. if (next.nodeType === DOMNodeTypes.ELEMENT) {
  114. const result = cb(next as Element)
  115. if (result === false) {
  116. break
  117. }
  118. } else if (isComment(next)) {
  119. if (next.data === ']') {
  120. if (--depth === 0) break
  121. } else if (next.data === '[') {
  122. depth++
  123. }
  124. }
  125. next = next.nextSibling
  126. }
  127. } else {
  128. cb(node as Element)
  129. }
  130. }