前言
最近,vue3
发布了 beta
版本,大家对其新特性的研究也越来越多,我也在思考着怎么将一些 vue2
的工具迁移至 vue3
中。刚好前几天思考到如何在 vue3
中给元素使用 clickoutside
事件,正好借这个机会简单记录一下。
开始搞起
我希望能够用 vue
的 @event
写法来进行事件的触发,组件的样子大概是这样的
<template>
<div @clickoutside="handleClickOutside"></div>
</template>
<script>
export default {
name: 'MyComponent',
setup () {
function handleClickOutside () {
// some action
}
return {
handleClickOutside
}
}
}
<script>
基本的思路就是要让事件直接从元素上派发, 那样 vue
就可以捕捉到事件, 并进行回调
所以首先要实现的是在原生 js
下的 clickoutside
的逻辑实现,相信这里的实现大家都懂,就不细说了,直接上代码~
先是事件的派发部分
// 用来存放需要 clickoutside 的元素
const elementSet = new Set()
document.addEventListener('click', event => {
const target = event.target
const path = event.path || (event.composedPath && event.composedPath())
elementSet.forEach(el => {
if (target !== el && (path ? !path.includes(el) : !el.contains(target))) {
dispatchEvent(el, { type: 'clickoutside' })
}
})
})
function dispatchEvent (el, payload = {}) {
const { type, bubbles = false, cancelable = false, ...data } = payload
const event = new Event(type, { bubbles, cancelable })
Object.assign(event, data)
return el.dispatchEvent(event)
}
然后就是如何把组件用的根元素放入 set
中,在 vue3
中获取根元素的方法与原来的有些不同
// vue2
export default {
mounted () {
console.log(this.$el)
}
}
// vue3
import { onMounted, getCurrentInstance } from 'vue'
export default {
setup () {
onMounted(() => {
const instance = getCurrentInstance()
console.log(instance.ctx.$el)
})
}
}
于是乎,只需要将根元素节点在适当的时候加入或移出 set
便大功告成
import { onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
function useClickOutside () {
const instance = getCurrentInstance()
onMounted(() => {
elementSet.add(instance.ctx.$el)
})
onBeforeUnmount(() => {
elementSet.delete(instance.ctx.$el)
})
}
再稍微改进一下,可以支持任意元素的绑定
import { ref, onMounted, onBeforeUnmount } from 'vue'
function useClickOutside () {
const wrapper = ref(null)
onMounted(() => {
elementSet.add(wrapper)
})
onBeforeUnmount(() => {
elementSet.delete(wrapper)
})
return wrapper
}
<template>
<div ref="wrapper" @clickoutside="handleClickOutside"></div>
</template>
<script>
import { useClickOutside } from 'clickoutside'
export default {
name: 'MyComponent',
setup () {
const wrapper = useClickOutside()
function handleClickOutside () {
// some action
}
return {
wrapper,
handleClickOutside
}
}
}
</script>
最后
其实只要把自定义事件派发逻辑的部分写好,就可以实现任意的自定义事件在 vue
中以 @event
的形式调用
完整代码直接贴出来
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
const isNull = any => typeof any === 'undefined' || any === null
const USE_TOUCH = isNull(window) && ('ontouchstart' in window || (isNull(navigator) && navigator.msMaxTouchPoints > 0))
const CLICK_TYPE = USE_TOUCH ? 'touchstart' : 'click'
const CLICK_OUTSIDE = 'clickoutside'
const events = {
[CLICK_OUTSIDE]: new Set()
}
document.addEventListener(CLICK_TYPE, event => {
const target = event.target
const type = CLICK_OUTSIDE
const path = event.path || (event.composedPath && event.composedPath())
events[type].forEach(el => {
if (target !== el && (path ? !path.includes(el) : !el.contains(target))) {
dispatchEvent(el, { type })
}
})
})
function observe (el, types) {
if (typeof types === 'string') {
types = [types]
}
Array.isArray(types) && types.forEach(type => {
events[type].add(el)
})
}
function disconnect (el, types) {
if (typeof types === 'string') {
types = [types]
}
Array.isArray(types) && types.forEach(type => {
events[type].delete(el)
})
}
function dispatchEvent (el, payload = {}, Event = window.Event) {
const { type, bubbles = false, cancelable = false, ...data } = payload
let event
if (!isNull(Event)) {
event = new Event(type, { bubbles, cancelable })
} else {
event = document.createEvent('HTMLEvents')
event.initEvent(type, bubbles, cancelable)
}
Object.assign(event, data)
return el.dispatchEvent(event)
}
export function useClickOutside () {
const wrapper = ref(null)
onMounted(() => {
nextTick(() => {
observe(wrapper.value, CLICK_OUTSIDE)
})
})
onBeforeUnmount(() => {
disconnect(wrapper.value, CLICK_OUTSIDE)
})
return wrapper
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。