组件, 理论上适用于任何语言框架, 只需要适配一下 <template> 及 <script> 的部分即可, 同时也是可以适用于原生 JS 的.
<template>
<script>
1<template>2 <div ref="targetRef">3 <slot />4 </div>5</template>6 7<script setup>8import { intersection } from 'lodash'9import { ref, unref, defineEmits, defineProps, onBeforeUnmount } from 'vue'10 72 collapsed lines11const targetRef = ref(null)12const emits = defineEmits(['trigger'])13 14// 这两个属性通常不会发生变化, 都是初始就预设好的, 所以就直接解构使用了, 在开发中不推荐这么使用15// eslint-disable-next-line vue/no-setup-props-destructure16const { ignore, capture } = defineProps({17 // 需要忽略的元素类名,多个类名以 , 分割18 ignore: {19 type: String,20 default: 'el-popper'21 },22 // 是否以捕获的形式触发事件23 capture: {24 type: Boolean,25 default: true26 }27})28 29// 过滤不合法的元素选择器30const IGNORES = ignore.split(',').filter(Boolean)31 32const clickEventHandler = async(event) => {33 const el = event.target34 35 // 实现的关键, 这个方法返回事件触发时, 在事件冒泡的完整路径36 const composed = event.composedPath() // ^[1] Event.composedPath()37 38 if (39 !el ||40 el === unref(targetRef) || // 如果点击的元素是当前组件本身41 composed.includes(unref(targetRef)) || // 如果点击的是组件的子元素时42 // 如果有设置忽略的元素类名并且被包含在内的话43 ignore && composed.some(element =>44 !!intersection(IGNORES, element.classList).length)45 // ↑ 这里也可以自己手写判断数据交集的实现, 主要是判断元素的类名是否处于忽略的列表中46 ) { return }47 48 emits('trigger', event)49}50 51// 因为我并不需要操作或获取当前组件的 dom 元素52// 又因为 document 是全局对象, 不管组件是否挂载都可以访问到53// 所以在添加事件的时候可以不用放在 onMounted() 里54document.addEventListener(55 'click',56 clickEventHandler,57 {58 capture,59 passive: true60 }61)62 63onBeforeUnmount(() => {64 // 组件卸载时一定记得清理副作用65 document.removeEventListener(66 'click',67 clickEventHandler,68 {69 capture,70 passive: true71 }72 )73})74</script>75 76<script>77// 给组件添加名字, 以及不在 DOM 元素上显式外部给组件设置的属性78export default {79 name: 'TrClickOutside',80 inheritAttrs: false81}82</script>