Vue3迁移后setTimeout防抖失效 复选框操作store commit立即触发
问题现象
我希望通过setTimeout实现防抖逻辑,控制store提交频次:当用户点击复选框时启动超时计时,等待用户停止操作一段时间后,再将选中的复选框状态commit到store,避免每次点击都触发状态更新。
- 首次操作时逻辑运行正常:会等待超时冷却结束后才触发一次commit
- 异常表现:第二次操作复选框时防抖逻辑失效,每次点击都会直接触发commit,不会等待超时计时完成
预期效果:仅当
setTimeout计时结束后才执行store commit操作。
该组件由正常运行的Vue 2版本迁移而来。
正常运行的Vue 2版本代码
<template> <PerfectScrollbar> <el-menu> <p v-if="hasSomeSelectedItem" class="select-all" @click="setAllItems('deselect')" > {{ _$t('deselect_all') }} </p> <p v-else class="deselect-all" @click="setAllItems('select')"> {{ _$t('select_all') }} </p> <el-menu-item v-for="item in items" :key="item.id"> <span v-if="type === 'datasets'" class="separator-char"> <p v-if="item.separatorChar"> {{ item.separatorChar }} </p> </span> <el-checkbox v-model="item.selected" @change="saveItemsState()"> {{ item.name }} </el-checkbox> </el-menu-item> </el-menu> </PerfectScrollbar> </template> <script> import _clonedeep from 'lodash/cloneDeep' import { capitalize } from '@/utils/string' import { PROCESS_ENV } from '@/utils/legacy/process-env' export default { props: { options: { type: Array, default: () => [], }, type: { type: String, default: '', }, }, data() { return { items: [], timeout: null, } }, computed: { hasSomeSelectedItem() { return this.items.some((item) => item.selected === true) }, getSelectedIds() { return this.items.filter((item) => item.selected).map((item) => item.id) }, }, mounted() { this.initializeItems() this.saveItemsStateNow() }, methods: { initializeItems() { const storedItemsIds = this.$store.getters['filters/get' + capitalize(this.type) + 'Many'] const optionsCloned = _clonedeep(this.options) if (storedItemsIds === 'all' || storedItemsIds.length === 0) { this.items = optionsCloned.map((item) => { item.selected = true return item }) this.saveItemsState() } else { this.items = optionsCloned.map((item) => { item.selected = storedItemsIds.includes(item.id) return item }) } }, saveItemsState() { // prevent update & store items for each click on item, wait 1 sec clearTimeout(this.timeout) this.timeout = setTimeout(() => { this.saveItemsStateNow() }, PROCESS_ENV.delay.oneSec) }, saveItemsStateNow() { this.setItemsStateAll() this.setItemsState() }, setItemsState() { this.$store.commit( 'filters/set' + capitalize(this.type) + 'Many', this.getSelectedIds ) }, setItemsStateAll() { const isAllSelected = this.getSelectedIds.length === this.items.length this.$store.commit( 'filters/set' + capitalize(this.type) + 'ManyAll', isAllSelected ) }, setAllItems(setter) { this.items.forEach((item) => { item.selected = setter === 'select' }) this.saveItemsState() }, _$t(label, params = {}) { return this.$t( `components.project_filters_sidebar.checkbox_menu.${label}`, params ) }, }, } </script>
存在防抖失效问题的Vue 3组件代码
<template> <PerfectScrollbar> <el-menu> <p v-if="hasSomeSelectedItem" class="select-all" @click="setAllItems('deselect')" > {{ _$t('deselect_all') }} </p> <p v-else class="deselect-all" @click="setAllItems('select')"> {{ _$t('select_all') }} </p> <el-menu-item v-for="item in items" :key="item.id"> <span v-if="type === 'datasets'" class="separator-char"> <p v-if="item.separatorChar"> {{ item.separatorChar }} </p> </span> <el-checkbox v-model="item.selected" @change="saveItemsState()"> {{ item.name }} </el-checkbox> </el-menu-item> </el-menu> </PerfectScrollbar> </template> <script lang="ts" setup> import { ItemsDisplayer } from '@/models/ItemsDisplayer' import _clonedeep from 'lodash/cloneDeep' import { capitalize } from '@/utils/capitalize' import { PropType, ref, onMounted, computed } from 'vue' import { useStore } from 'vuex' import { useI18n } from 'vue-i18n' const store = useStore() const { t } = useI18n() const props = defineProps({ options: { type: Array as PropType<ItemsDisplayer[]>, default: () => [], }, type: { type: String, default: '', }, }) let items = ref<ItemsDisplayer[]>() let timeout: ReturnType<typeof setTimeout> onMounted(() => { initializeItems() saveItemsStateNow() }) function initializeItems() { const storedItemsIds = store.getters['filters/get' + capitalize(props.type) + 'Many'] const optionsCloned = _clonedeep(props.options) if (storedItemsIds === 'all' || storedItemsIds.length === 0) { items.value = optionsCloned.map((item) => { item.selected = true return item }) saveItemsState() } else { items.value = optionsCloned.map((item) => { item.selected = storedItemsIds.includes(item.id) return item }) } } function saveItemsState() { console.log('TIMEOUT ', timeout) // prevent update & store items for each click on item, wait 1 sec clearTimeout(timeout) console.log('TIMEOUT AFTER CLEAR', timeout) timeout = setTimeout(() => { console.log('DINS TIMEOUT ', timeout) saveItemsStateNow() }, 3000) } const hasSomeSelectedItem = computed(() => { return items.value?.some((item) => item.selected === true) }) const getSelectedIds = computed(() => { return items.value?.filter((item) => item.selected).map((item) => item.id) }) function saveItemsStateNow() { console.log('saveItemsStateNow') setItemsStateAll() setItemsState() } function setItemsState() { store.commit('filters/set' + capitalize(props.type) + 'Many', getSelectedIds) } function setItemsStateAll() { const isAllSelected = getSelectedIds.value?.length === items.value?.length store.commit( 'filters/set' + capitalize(props.type) + 'ManyAll', isAllSelected ) } function setAllItems(setter: string) { items.value?.forEach((item) => { item.selected = setter === 'select' }) saveItemsState() } function _$t(label: string, params = {}) { return t(`components.project_filters_sidebar.checkbox_menu.${label}`, params) } </script>
问题根因
防抖逻辑本身没有问题,异常来自两个编码错误:
- 核心错误:将响应式ref对象存入了Vuex store
Vue2中this.getSelectedIds直接返回计算后的普通ID数组,存入store的是静态值;但Vue3的composition API中,getSelectedIds是ComputedRef类型的响应式对象,你在setItemsState方法中直接传递getSelectedIds而非getSelectedIds.value,导致把整个响应式ref存入了store。
首次防抖结束执行commit后,store中存储的是这个ref的引用,后续你每次修改复选框选中状态,ref的value会自动更新,store中的值也会同步变化,造成了「每次点击都触发commit」的假象,实际上commit只执行了一次,后续的状态更新是响应式引用自动同步导致的。 - 次要逻辑问题:
itemsref初始值为undefined,timeout变量未初始化,和Vue2逻辑不对齐,极端场景会出现意外onMounted中调用initializeItems时如果走默认全选分支,会先创建一个延迟提交的timeout,之后同步调用saveItemsStateNow时没有清除这个timeout,会导致延迟到点后重复提交
修复方案
修改对应代码即可:
- 给
items和timeout设置初始值,对齐Vue2逻辑:
let items = ref<ItemsDisplayer[]>([]) let timeout: ReturnType<typeof setTimeout> | null = null
- 修改
setItemsState方法,传入computed的实际值,建议拷贝一份普通数组避免响应式引用污染store:
function setItemsState() { store.commit( 'filters/set' + capitalize(props.type) + 'Many', [...(getSelectedIds.value || [])] ) }
- 修改
onMounted逻辑,调用saveItemsStateNow前先清除可能存在的timeout,避免重复提交:
onMounted(() => { initializeItems() if (timeout) clearTimeout(timeout) saveItemsStateNow() })
- 可选优化:
saveItemsState中清除timeout后可以把timeout重置为null,方便调试:
function saveItemsState() { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => { saveItemsStateNow() timeout = null }, 3000) }
内容的提问来源于stack exchange,提问作者Airea
相关产品推荐
相关产品推荐

