Skip to page content

useMutation 组合式 API
v2.34+

useMutation() composable 会监视对某个元素(或某个组件)DOM 树所做的更改:子节点的增删、属性的变化、文本的变化。在底层它使用 Mutation Observer API。

它是 v-mutation 指令在 setup 代码中的对应物。当你想观察组件自身的根元素,或任意元素/组件的 ref,而又不想动模板时,就用这个 composable。

TIP

在 SSR 或 SSG 模式的服务端,该 composable 不会观察任何东西。

在组件之外使用

该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。此时没有可回退的组件根元素,也没有挂载时机可等待,因此请提供一个 target(一个元素,或元素的 ref、getter);观察会立即开始,并且不会自行停止:完成后请调用 stopMutation()。

语法

import { useTemplateRef } from 'vue'
import { useMutation } from 'quasar'

setup () {
  const target = useTemplateRef('target') // an Element or a component

  const { mutationRecords, stopMutation } = useMutation({
    // all optional:
    target,               // omit it to observe the component's own root element

    // MutationObserver options; with none of them set,
    // every kind of change gets observed
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
    characterDataOldValue: true,
    attributeFilter: [ 'class', 'style' ],

    once: false,          // stop after the first batch of records
    disabled: false,      // pause observing
    onMutation (records) { // called with the Array of MutationRecord
      // return false to stop observing for good
    }
  })

  // ...
}
function useMutation(
  options?: MaybeRefOrGetter<{
    target?: MaybeRefOrGetter<
      Element | ComponentPublicInstance | null | undefined
    >
    childList?: boolean
    attributes?: boolean
    characterData?: boolean
    subtree?: boolean
    attributeOldValue?: boolean
    characterDataOldValue?: boolean
    attributeFilter?: string[]
    once?: boolean
    disabled?: boolean
    onMutation?: (records: MutationRecord[]) => false | void
  }>
): {
  mutationRecords: ShallowRef<MutationRecord[]>
  stopMutation: () => void
}

不传 target 时,该 composable 会观察它所在组件的根元素(以组件挂载的那一刻为准)。渲染 fragment(多个根节点)的组件没有可供观察的根元素,因此在那种场景下请提供一个 target。

先阅读 Mutation Observer API 有助于你理解这些观察选项。当 childList、attributes、characterData、subtree、attributeOldValue、characterDataOldValue 和 attributeFilter 都没有设置时,每一种更改都会被观察,并且会包含旧值,这与不带修饰符的 v-mutation 指令行为一致。设置其中任意一项,就只观察你所要求的内容;此时 childList、attributes 或 characterData 中至少要设置一个(或设置 attributeFilter,它隐含了 attributes),因为单独设置 subtree 或那些旧值标志会让原生观察器抛出异常。

浏览器在这些更改之后交付的每一批 MutationRecord 都会落到响应式的 mutationRecords 中(仅保留最后一批,因此它永远不会不断增长),并交给 onMutation 处理函数。从处理函数中返回 false 会彻底停止观察。设置了 once 时,观察会在第一批之后自行停止。

stopMutation() 会彻底结束观察。你很少会需要它,因为组件销毁时该 composable 会自行停止。

警告!避免反馈循环

你的处理函数(或它更新的响应式状态)对被观察的 DOM 所做的任何操作,都会构成一次新的变更,从而再次调用处理函数,如此循环往复、没有尽头:例如在被观察元素内部渲染这些状态、在它上面切换某个 class、往它里面追加一个节点。请把处理函数的副作用保持在被观察元素之外,或者只观察你真正需要的部分(元素自身的 attributes、它的直接 childList)。

运行时更改选项

选项可以是一个普通对象、一个 Ref 或一个 getter 函数。普通对象只读取一次。使用 Ref 或 getter 时,该 composable 会追踪选项所读取的任何响应式状态,并在该状态变化时重新应用它们,因此你永远不需要调用任何东西来「更新」它:

  • 切换 disabled 会暂停和恢复观察(已经触发过的 once 在 once 保持开启期间会一直停止;把 once 重新设为 false 会重新开始观察)
  • 把 target 指向另一个元素(或让某个 template ref 通过 v-if 发生变化)会随之跟踪
  • 更改被观察的内容会立即生效,并保留那些尚未交付的更改
  • 替换 onMutation 从下一批起生效
import { ref } from 'vue'
import { useMutation } from 'quasar'

setup () {
  const paused = ref(false)

  useMutation(() => ({
    childList: true,
    disabled: paused.value,
    onMutation (records) { /* ... */ }
  }))

  // ...
}

示例

下面这个示例通过一个 template ref 观察一个列表:添加、删除和重命名列表项,并在浏览器通过 mutationRecords 上报时看到每一批更改:

<template>
  <div class="q-pa-md">
    <div class="q-gutter-sm q-mb-md">
      <q-btn color="primary" push label="Add item" @click="addItem" />
      <q-btn
        color="negative"
        push
        label="Remove last"
        :disable="items.length === 0"
        @click="removeItem"
      />
      <q-btn
        color="secondary"
        push
        label="Rename first"
        :disable="items.length === 0"
        @click="renameItem"
      />
      <q-toggle v-model="paused" label="Paused" />
    </div>

    <q-list ref="listRef" bordered separator class="q-mb-md">
      <q-item v-for="item in items" :key="item.id">
        <q-item-section>{{ item.label }}</q-item-section>
      </q-item>
      <q-item v-if="items.length === 0">
        <q-item-section class="text-grey">Empty list</q-item-section>
      </q-item>
    </q-list>

    <div class="q-gutter-sm">
      <q-badge :label="`batches: ${batches}`" />
      <q-badge color="secondary" :label="`last batch: ${lastBatch}`" />
      <q-badge
        color="accent"
        :label="`records in it: ${mutationRecords.length}`"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef } from 'vue'
import { useMutation } from 'quasar'

const listRef = useTemplateRef('listRef') // a component: its root gets observed

const items = ref([])
const paused = ref(false)
const batches = ref(0)
const lastBatch = ref('none')

let nextId = 1

const { mutationRecords } = useMutation(() => ({
  target: listRef,
  childList: true,
  characterData: true,
  subtree: true,
  disabled: paused.value,
  onMutation(records) {
    batches.value++
    lastBatch.value = records
      .map(record => record.type)
      .filter((type, index, list) => list.indexOf(type) === index)
      .join(', ')
  }
}))

function addItem() {
  items.value.push({ id: nextId, label: `Item ${nextId}` })
  nextId++
}

function removeItem() {
  items.value.pop()
}

function renameItem() {
  items.value[0].label += ' (renamed)'
}
</script>