likes
comments
collection
share

vue3中addEventListener的妙用

作者站长头像
站长
· 阅读数 33

缘起

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()
        }
      }
    )
  }
}

如上指令,在销毁时无法进行事件的移除,导致事件处理函数无法被销毁,占用内存无法释放:

vue3中addEventListener的妙用

寻解

查看mdn# addEventListener的解释,发现了一个神奇的参数:

  • signal 可选: AbortSignal,该 AbortSignalabort() 方法被调用时,监听器会被移除。

这不就可以完美解决了么,立马优化指令:

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

场景

如下场景:

vue3中addEventListener的妙用

<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>
      &nbsp;
      <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"
      />
      &nbsp;
      <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
评论
请登录