Nuxt 项目实战 - 14:实现两个非常实用的自定义指令

场景

  1. 光标移入一个元素执行执行一段逻辑,比如:按钮颜色要变,其他弹框要显示,光标移出元素时又恢复。如果仅仅是样式不同可直接css,但是要执行js代码就有点麻烦了。可能你立马会想到mouseenter和mouseleave。但是如果有多层元素都需要这个就没有那么优雅了

  2. 点击一个按钮弹出modal对话框,背景是黑色透明,只有中间一部分是显示的内容,点击内容外面就关闭对话框。

实现自定义自定v-hover-el

  • 使用方式

    html 复制代码
    <template>
      <div class="root">
        <button v-hover-el="(v: boolean) => isHoverEl = v">isHoverEl: {{ isHoverEl }}</button>
      </div>
    </template>
    <script setup lang="ts">
    const isHoverEl = ref(false);
    </script>
  • 代码实现

    tsx 复制代码
    import type { ObjectDirective, DirectiveBinding } from 'vue';
    
    type FlushList = Map<
    	HTMLElement,
    	{
    		bindingFn: (isMouseEnter: boolean, event: MouseEvent) => void,
    		enterFn: (event: MouseEvent) => void,
    		leaveFn: (event: MouseEvent) => void,
    	}
    >
    
    const nodeList: FlushList = new Map();
    
    function executeHandler(isMouseEnter: boolean, event: MouseEvent) {
    	let target = event.target as HTMLElement;
    	for (let [key, value] of nodeList.entries()) {
    		//! 说明:如果触发事件target是遍历的元素时就触发调用绑定的方法
    		if (key == target) {
    			value.bindingFn(isMouseEnter, event);
    		}
    	}
    }
    
    function addListener(el: HTMLElement, enterListener: any, leaveListener: any) {
    	el.addEventListener("mouseenter", enterListener);
    	el.addEventListener("mouseleave", leaveListener);
    }
    
    function delListener(el: HTMLElement, enterListener: any, leaveListener: any) {
    	el.removeEventListener("mouseenter", enterListener);
    	el.removeEventListener("mouseleave", leaveListener);
    }
    
    const HoverEl: ObjectDirective = {
    	beforeMount(el: HTMLElement, binding: DirectiveBinding) {
    		let options = {
    			bindingFn: binding.value,
    			enterFn: executeHandler.bind(null, true),
    			leaveFn: executeHandler.bind(null, false)
    		}
    		//! 挂载阶段:
    		//! 1.记录设置了指令的元素
    		nodeList.set(el, options);
    		//! 2.给元素添加移入移出事件
    		addListener(el, options.enterFn, options.leaveFn);
    	},
    	unmounted(el: HTMLElement) {
    		let options = nodeList.get(el);
    		//! 卸载阶段:
    		//! 需要把事件移除掉
    		if (options) {
    			delListener(el, options.enterFn, options.leaveFn);
    		}
    		nodeList.delete(el);
    	}
    }
    
    export { HoverEl }

实现自定义指令v-click-outside

  • 使用方式

    html 复制代码
    <template>
      <div class="root">
        <button @click="isDialogVis = true">open dialog</button>
        <div class="dialog"
             v-if="isDialogVis">
          <div class="wrapper"
               v-click-outside="() => isDialogVis = false">
            This is a dialog
            <div class="tips">
              tips: the dialog will close if you click outside the border.
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    const isDialogVis = ref(false);
    </script>
  • 代码实现

    tsx 复制代码
    import type { ObjectDirective, DirectiveBinding } from 'vue';
    import { isClient } from '@vueuse/core'
    
    type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
    type FlushList = Map<
    	HTMLElement,
    	{
    		documentHandler: DocumentHandler
    		bindingFn: (...args: unknown[]) => unknown
    	}
    >
    
    const nodeList: FlushList = new Map()
    //! 记录点击的开始事件对象
    let startClick: MouseEvent;
    
    if (isClient) {
    	//! 点击事件绑定在document上
    	document.addEventListener('mousedown', (e: MouseEvent) => (startClick = e))
    	document.addEventListener('mouseup', (e: MouseEvent) => {
    		for (const handler of nodeList.values()) {
    			const { documentHandler } = handler;
    			documentHandler(e as MouseEvent, startClick)
    		}
    	});
    }
    
    function createDocumentHandler(
    	el: HTMLElement,
    	binding: DirectiveBinding
    ): DocumentHandler {
    	let excludes: HTMLElement[] = []
    	if (Array.isArray(binding.arg)) {
    		excludes = binding.arg
    	} else if (isElement(binding.arg)) {
    		excludes.push(binding.arg as unknown as HTMLElement)
    	}
    
    	//! 事件处理
    	return function (mouseup, mousedown) {
    		const mouseUpTarget = mouseup.target as Node
    		const mouseDownTarget = mousedown?.target as Node
    		const isContainedByEl =
    			el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
    		const isSelf = el === mouseUpTarget;
    		//! 如果点击是父容器元素 or 元素本身不触发事件,否则:就是点击元素外面了,需要抛事件
    		if (isContainedByEl || isSelf) {
    			return;
    		}
    		binding.value(mouseup, mousedown);
    	}
    }
    
    const ClickOutside: ObjectDirective = {
    	beforeMount(el: HTMLElement, binding: DirectiveBinding) {
    		//! 挂载阶段:
    		//! 记录设置了指令的元素 && 给元素绑定事件
    		nodeList.set(el, {
    			documentHandler: createDocumentHandler(el, binding),
    			bindingFn: binding.value,
    		});
    	},
    	unmounted(el: HTMLElement) {
    		//! 卸载阶段:
    		//! 需要把元素移除
    		nodeList.delete(el)
    	}
    }
    
    export { ClickOutside }

注册自定义指令

tsx 复制代码
import { defineNuxtPlugin } from '#app'
import { ClickOutside } from '~/composables/directives/click-outside'
import { HoverEl } from '~/composables/directives/hover-el'

export default defineNuxtPlugin((nuxtApp) => {
	nuxtApp.vueApp.directive("click-outside", ClickOutside)
	nuxtApp.vueApp.directive("hover-el", HoverEl)
})

说明:我这个是Nuxt3 中的注册方式,Vue 改成app.directive的方式就可以了

总结

  • 不知到怎么弄就给别人多借鉴借鉴
  • 实现功能只是最基本的完成任务,如果能用更优雅的方式解决那么就发挥你的潜力去实现,或许这是你提升的有效方式

demo 传送门

参考文献

相关推荐
雪碧聊技术18 分钟前
深入解析Vue中v-model的双向绑定实现原理
前端·javascript·vue.js·v-model
快起来别睡了20 分钟前
手写 Ajax 与 Promise:从底层原理到实际应用
前端
打不着的大喇叭1 小时前
uniapp的光标跟随和打字机效果
前端·javascript·uni-app
无我Code1 小时前
2025----前端个人年中总结
前端·年终总结·创业
程序猿阿伟1 小时前
《前端路由重构:解锁多语言交互的底层逻辑》
前端·重构
Sun_light1 小时前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js
爱学习的茄子2 小时前
深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南
前端·javascript·面试
莫空00002 小时前
Vue组件通信方式详解
前端·面试
呆呆的心2 小时前
揭秘 CSS 伪元素:不用加标签也能玩转出花的界面技巧 ✨
前端·css·html
百锦再2 小时前
重新学习Vue中的按键监听和鼠标监听
javascript·vue.js·vue·计算机外设·click·up·down