hydrationStrategies.ts 3.6 KB

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