vue3中addEventListener的妙用
缘起
vue3中定义全局指令时,往往会碰到一个问题:事件无法解绑
。为什么会这样,因为通常在指令的mounted
钩子中绑定事件,事件处理函数也定义在mounted中。在指令的unmounted
钩子函数中无法获取到定义的事件处理函数,所以绑定的事件无法销毁,尤其当这个事件是绑定在父组件,或者body上时,无法解绑事件,指令大量使用会造成性能问题。
removeEventListener
方法可以删除使用addEventListener
方法添加的事件。使用事件类型,事件侦听器函数本身,以及可能影响匹配过程的各种可选择的选项的组合来标识要删除的事件侦听器,简单说要使用removeEventListener
移除事件,需要事件处理函数和添加时的事件处理函数是同一个。
如下一个判断鼠标点击区域是否在指定区域内的指令:
// 使用场景:仅在点击select组件外部区域,收起下拉选项
import { Directive } from 'vue'
export const clickOutside: Directive = {
mounted(el: Element, { value }) {
const controller = new AbortController()
controllerMap.set(el, controller)
document.body.addEventListener(
'click',
(e) => {
// 在外部区域点击了
if (!el.contains(e.target as Element)) {
typeof value === 'function' && value()
}
}
)
}
}
如上指令,在销毁时无法进行事件的移除,导致事件处理函数无法被销毁,占用内存无法释放:
寻解
查看mdn
中 # addEventListener
的解释,发现了一个神奇的参数:
- signal 可选:
AbortSignal
,该AbortSignal
的abort()
方法被调用时,监听器会被移除。
这不就可以完美解决了么,立马优化指令:
import { Directive } from 'vue'
const controllerMap = new WeakMap<Element, AbortController>()
export const clickOutside: Directive = {
mounted(el: Element, { value }) {
const controller = new AbortController()
controllerMap.set(el, controller)
document.body.addEventListener(
'click',
(e) => {
// 在外部区域点击了
if (!el.contains(e.target as Element)) {
typeof value === 'function' && value()
}
},
{
capture: true, // 为了防止v-if切换导致的判断误差,在捕获阶段触发事件
signal: controller.signal
}
)
},
beforeUnmount(el) {
const controller = controllerMap.get(el)
controller?.abort() // 移除事件
}
}
如上,在指令销毁时,触发beforeUnmount
事件,调用控制对象控制器对象
的abort
方法移除事件
注意:当我们点击某块区域的时候,需要先判断是否在指定的区域内,然后再执行相应的逻辑,这时要求判断是否在区域内的点击事件,要优先执行,所以要设置指令的点击事件在
事件捕获阶段
触发, 即:capture: true
场景
如下场景:
<template>
<div class="edit-cell" v-clickOutside="clickOutside">
<template v-if="!edit">
<a-tooltip :title="value" v-if="value.length > 20">
<div class="text text-ellipsis">
{{ value || '--' }}
</div>
</a-tooltip>
<div v-else>{{ value || '--' }}</div>
<edit-outlined class="editable-cell-icon icon" @click="enableEdit" />
</template>
<template v-else>
<component
:is="EditComponent"
ref="editComponentRef"
:placeholder="$t('correction.create.modal.form.projectName.err')"
class="text-edit"
:maxLength="200"
@input="change"
style="height: 60px; resize: none"
:value="currentValue"
@pressEnter="pressEnter"
/>
<check-outlined class="editable-cell-icon-check icon" @click="save" />
</template>
</div>
</template>
<script lang="ts" setup>
import { EditOutlined, CheckOutlined } from '@ant-design/icons-vue'
const props = defineProps({
value: {
type: String,
default: ''
},
type: {
type: String as PropType<'input' | 'textarea'>,
default: 'input'
}
})
const emits = defineEmits<{
change: [value: string]
}>()
const currentValue = ref<string>('')
const EditComponent = computed(() =>
props.type === 'input' ? 'NWInput' : 'ATextarea'
)
const edit = ref<boolean>(false)
const change = ({ target }) => {
currentValue.value = target?.value ?? ''
}
const pressEnter = () => {
emits('change', currentValue.value)
edit.value = false
}
const save = () => {
emits('change', currentValue.value)
edit.value = false
}
const editComponentRef = shallowRef()
const enableEdit = () => {
currentValue.value = props.value
edit.value = true
nextTick(() => {
editComponentRef.value.focus()
})
}
const clickOutside = () => {
edit.value = false
}
</script>
<style lang="less" scoped>
.edit-cell {
display: flex;
justify-content: flex-start;
align-items: center;
.text-edit,
.text {
flex: 1;
}
&:hover {
.icon {
visibility: visible;
}
}
.icon {
visibility: hidden;
}
}
.text-ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left !important;
}
</style>
另一种思路
可以将处理函数定义在指令外,所有指令共用同一个处理函数,参数绑定在处理函数上,类似vue2
中依赖收集时的:Dep.target
,将当前处理的watcher
存储在Dep
函数的target
属性中,当然这种方式只适用于串行的场景
转载自:https://juejin.cn/post/7269411820905316389