useMutation() composable 会监视对某个元素(或某个组件)DOM 树所做的更改:子节点的增删、属性的变化、文本的变化。在底层它使用 Mutation Observer API。
它是 v-mutation 指令在 setup 代码中的对应物。当你想观察组件自身的根元素,或任意元素/组件的 ref,而又不想动模板时,就用这个 composable。
在 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>