Skip to content

DOM监听 useMutationObserver

赞助

使用 MutationObserver 观察 DOM 元素的变化的组合式函数

Show Source Code
ts
/**
 * 组合式函数
 * 使用 MutationObserver 观察 DOM 元素的变化
 *
 * 该函数提供了一个便捷的方式来订阅 DOM 元素的变动,当元素发生指定的变化时,调用提供的回调函数
 * 使用者可以指定要观察的一个或多个 DOM 元素,以及观察的选项和回调函数
 *
 * @param {Ref | Ref[] | HTMLElement | HTMLElement[]} target 要观察的目标,可以是 Ref 对象、Ref 数组、HTMLElement 或 HTMLElement 数组
 * @param {MutationCallback} callback 当观察到变化时调用的回调函数
 * @param {object} [options = {}] MutationObserver 的观察选项,默认为空对象;例如:
 *          subtree: 是否监听以 target 为根节点的整个子树,包括子树中所有节点的属性
 *          childList: 是否监听 target 节点中发生的节点的新增与删除
 *          attributes: 是否观察所有监听的节点属性值的变化
 *          attributeFilter: 声明哪些属性名会被监听的数组;如果不声明该属性,所有属性的变化都将触发通知
 * @returns {start: () => void, stop: () => void} 返回一个对象,包含停止和开始观察的方法,使用者可以调用 start 方法开始观察,调用 stop 方法停止观察
 */
import { ref, toValue, computed, watch, onBeforeUnmount, onMounted, getCurrentInstance } from 'vue'
import type { Ref, ComputedRef } from 'vue'
/**
 * 归一化观察目标为 HTMLElement 数组
 *
 * 兼容 Ref / Ref[] / HTMLElement / HTMLElement[] 四种入参:先解包 Ref,再过滤空值,
 * 保证后续 observe 调用拿到的都是可用元素。
 */
function resolveTargetElements(target: Ref | Ref[] | HTMLElement | HTMLElement[]): HTMLElement[] {
  const targetValue = toValue(target) as Ref | Ref[] | HTMLElement | HTMLElement[] | null | undefined
  if (!targetValue) return []
  const list = Array.isArray(targetValue) ? targetValue : [targetValue]
  return list
    .map((item) => toValue(item) as HTMLElement | null | undefined)
    .filter((element): element is HTMLElement => Boolean(element))
}
export function useMutationObserver(
  target: Ref | Ref[] | HTMLElement | HTMLElement[],
  callback: MutationCallback,
  options: object = {}
): { start: () => void; stop: () => void } {
  // 用 typeof 判断而非裸 window:SSR(Node)下裸引用会直接抛 ReferenceError
  const isSupported = useSupported(() => typeof window !== 'undefined' && 'MutationObserver' in window)
  const stopObservation = ref(false)
  let observer: MutationObserver | undefined
  const targets = computed(() => resolveTargetElements(target))
  // 定义清理函数,用于断开 MutationObserver 的连接
  const cleanup = () => {
    if (observer) {
      observer.disconnect()
      observer = undefined
    }
  }
  // 初始化 MutationObserver,开始观察目标元素
  const observeElements = () => {
    if (isSupported.value && targets.value.length && !stopObservation.value) {
      observer = new MutationObserver(callback)
      targets.value.forEach((element: HTMLElement) => observer!.observe(element, options))
    }
  }
  // 监听 targets 的变化,当 targets 变化时,重新建立 MutationObserver 观察
  watch(
    () => targets.value,
    () => {
      cleanup()
      observeElements()
    },
    {
      immediate: true, // 立即触发回调,以便初始状态也被观察
      flush: 'post'
    }
  )
  const start = () => {
    stopObservation.value = false
    observeElements()
  }
  const stop = () => {
    stopObservation.value = true
    cleanup()
  }
  // 在组件卸载前清理 MutationObserver
  onBeforeUnmount(() => cleanup())
  return {
    start,
    stop
  }
}
export function useSupported(callback: () => unknown): ComputedRef<boolean> {
  const isMounted = useMounted()
  return computed(() => {
    // to trigger the ref
    isMounted.value
    return Boolean(callback())
  })
}
export function useMounted(): Ref<boolean> {
  const isMounted = ref(false)
  // 获取当前组件的实例
  const instance = getCurrentInstance()
  if (instance) {
    onMounted(() => {
      isMounted.value = true
    }, instance)
  }
  return isMounted
}

基本使用

监听容器内节点的增删,并实时统计变更次数


新增节点
删除节点
停止观察
开始观察
已观察到 0 条变更记录
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useMutationObserver } from 'vue-amazing-ui'
const observerRef = ref<HTMLDivElement | null>(null)
const mutationCount = ref(0)
const itemId = ref(0)
const callback = (mutationsList: MutationRecord[]) => {
  mutationCount.value += mutationsList.length
}
const options = { childList: true, attributes: true, subtree: true }
const { start, stop } = useMutationObserver(observerRef, callback, options)
function addItem() {
  const item = document.createElement('div')
  item.className = 'observer-item'
  item.textContent = `节点 ${++itemId.value}`
  observerRef.value?.appendChild(item)
}
function removeItem() {
  observerRef.value?.lastElementChild?.remove()
}
</script>
<template>
  <Flex vertical>
    <Space :gap="8">
      <Button type="primary" @click="addItem">新增节点</Button>
      <Button @click="removeItem">删除节点</Button>
      <Button @click="stop">停止观察</Button>
      <Button @click="start">开始观察</Button>
    </Space>
    <Alert type="info" :message="`已观察到 ${mutationCount} 条变更记录`" />
    <div ref="observerRef" class="observer-container"></div>
  </Flex>
</template>
<style lang="less" scoped>
.observer-container {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 52px;
  padding: 8px 12px;
  border: 1px dashed rgba(0, 0, 0, 0.15);
  border-radius: 6px;
  :deep(.observer-item) {
    height: fit-content;
    padding: 4px 10px;
    font-size: 14px;
    background: #f0f5ff;
    border-radius: 4px;
  }
}
</style>

Params

参数说明类型默认值
target要观察的 DOM 元素或元素数组,可以是 ref 引用,也可以是 DOM 元素本身Ref | Ref[] | HTMLElement | HTMLElement[]undefined
callback当观察到变化时调用的回调函数MutationCallbackundefined
options观察选项,默认为空对象,参考文档object{}

Return

名称说明类型
start开始观察目标元素() => void
stop停止观察并断开与目标元素的连接() => void

参考文档

Released under the MIT License.