资讯动态

Vue 3 深入学习笔记 11:自定义指令与 DOM 行为封装

发布时间:2026/10/20 11:17:33 来源:尧图企业网站定制
Vue 3 深入学习笔记 11自定义指令与 DOM 行为封装一、为什么需要自定义指令在 Vue 中可以从三个角度理解常见的复用方式方案主要职责典型场景Component组件封装 UI 结构与交互弹窗、表格、表单Composable组合式函数封装可复用的状态与业务逻辑分页、请求、搜索条件Custom Directive自定义指令封装 DOM 层面的行为自动聚焦、点击外部关闭、DOM 属性处理自定义指令尤其适合那些需要直接操作 DOM 元素的场景。例如页面打开后输入框自动获得焦点。点击某个元素外部时关闭面板。为元素添加或移除事件监听器。根据需要设置原生 DOM 属性。二、自定义指令是不是相当于添加 listener可以这样理解一部分场景但自定义指令不等于添加事件监听器。例如下面这段代码直接添加了一个事件监听器consthandler(event){console.log(点击了页面,event)}document.addEventListener(click,handler)而自定义指令可以把添加监听器、保存处理函数以及卸载时清理监听器等操作统一封装起来constvClickOutside{mounted(el,binding){consthandler(event){if(!el.contains(event.target)){binding.value()}}document.addEventListener(click,handler)el.__clickOutsideHandler__handler},unmounted(el){document.removeEventListener(click,el.__clickOutsideHandler__)deleteel.__clickOutsideHandler__}}因此更准确的理解是自定义指令是 Vue 提供的一种 DOM 行为封装机制添加 listener 只是它能完成的事情之一。三、从最简单的自定义指令开始1. 自动聚焦如果只有一个输入框需要自动聚焦可以直接在组件中实现script setup import { onMounted, ref } from vue const inputRef ref(null) onMounted(() { inputRef.value?.focus() }) /script template input refinputRef / /template如果很多组件都有相同需求就可以封装成自定义指令script setup const vFocus { mounted(el) { el.focus() } } /script template input v-focus / /template这里的el就是绑定指令的原生 DOM 元素。mounted在元素挂载到 DOM 后执行所以可以在这里调用focus()。注意这个例子没有添加任何 listener但它仍然是自定义指令。因此自定义指令并不等于事件监听器。2. 限制输入框最大长度script setup const vMaxLength { mounted(el, binding) { el.maxLength binding.value }, updated(el, binding) { el.maxLength binding.value } } /script template input v-max-length20 / /templatebinding.value是传入的值这里是20。如果绑定值会变化可以在updated钩子中同步更新 DOM 属性。四、自定义指令的生命周期钩子触发时机created元素属性、事件监听器等初始化之前beforeMount元素挂载到 DOM 之前mounted元素挂载到 DOM 之后beforeUpdate元素所在组件更新之前updated元素所在组件更新之后beforeUnmount元素卸载之前unmounted元素卸载之后例如constvFocus{mounted(el){el.focus()}}最常用的两个钩子是mounted执行初始化 DOM 行为例如聚焦、添加事件监听器。unmounted执行清理例如移除事件监听器、释放相关资源。五、指令绑定参数 binding自定义指令的钩子通常会收到el、binding等参数。属性含义binding.value当前绑定值binding.oldValue更新前的绑定值binding.arg指令参数binding.modifiers指令修饰符集合binding.instance使用该指令的组件实例例如input v-example:foo.barmessage /对应关系binding.arg// foobinding.modifiers// { bar: true }binding.value// message 的当前值指令可以通过binding.value接收函数因此组件可以把行为交给指令执行div v-click-outsideclosePanel 面板内容 /div其中closePanel是组件中的函数指令检测到点击发生在元素外部时就可以调用它。六、实战点击元素外部时关闭面板这是下拉菜单、弹出面板等 UI 中常见的需求。script setup import { ref } from vue const visible ref(true) function closePanel() { visible.value false } const vClickOutside { mounted(el, binding) { const handler (event) { if (!el.contains(event.target)) { binding.value() } } document.addEventListener(click, handler) el.__clickOutsideHandler__ handler }, unmounted(el) { document.removeEventListener( click, el.__clickOutsideHandler__ ) delete el.__clickOutsideHandler__ } } /script template div v-ifvisible v-click-outsideclosePanel 这是一个面板 /div /template关键点document.addEventListener(click, handler)监听整个文档的点击事件。el.contains(event.target)判断点击目标是否位于当前元素内部。binding.value()调用组件传入的关闭函数。document.removeEventListener(...)移除监听器避免元素卸载后监听器仍然存在。示例中的el.__clickOutsideHandler__是为了演示而使用的自定义 DOM 属性。实际项目可以使用WeakMap保存元素与事件处理函数的对应关系避免给 DOM 元素增加自定义属性。更稳妥的事件处理思路如果使用WeakMap保存处理函数可以这样写consthandlersnewWeakMap()constvClickOutside{mounted(el,binding){consthandler(event){if(!el.contains(event.target)){binding.value()}}handlers.set(el,handler)document.addEventListener(click,handler)},unmounted(el){consthandlerhandlers.get(el)if(handler){document.removeEventListener(click,handler)handlers.delete(el)}}}注意这仍然是用于理解原理的简化实现。实际项目还需要考虑点击触发按钮与面板关闭之间的事件顺序、事件冒泡、键盘操作和无障碍访问等问题。七、局部注册与全局注册1. 局部注册在script setup中按vXxx的形式定义变量即可在模板中使用对应的v-xxx指令script setup const vFocus { mounted(el) { el.focus() } } /script template input v-focus / /template2. 全局注册如果多个页面都需要使用某个指令可以在应用初始化时注册import{createApp}fromvueimportAppfrom./App.vueconstappcreateApp(App)app.directive(focus,{mounted(el){el.focus()}})app.mount(#app)之后组件可以直接使用input v-focus /建议只有确实需要跨页面复用的指令才全局注册避免全局指令过多而难以维护。八、组件、Composable、自定义指令的选择可以用下面这几个问题判断是否需要封装一套 UI 结构和交互优先考虑组件。是否需要复用响应式状态、计算逻辑或业务流程优先考虑 Composable。是否需要直接操作 DOM且这种行为需要在多个元素或页面复用可以考虑自定义指令。例如需求更合适的方案原因支持搜索、分页、排序的表格Component有完整的 UI 结构与交互多个页面共用搜索请求逻辑Composable主要复用状态与业务逻辑点击元素外部关闭面板Custom Directive 或组件内部逻辑主要处理 DOM 事件与元素关系输入框自动聚焦Custom Directive 或组件生命周期属于 DOM 行为根据响应式权限控制按钮通常优先用模板条件或权限组件不一定需要直接操作 DOM九、自定义指令的设计原则优先使用 Vue 原生能力。能通过v-if、v-show、v-model或组件属性实现的需求不必强行使用指令。指令主要负责 DOM 行为。不要把复杂业务流程、接口请求和业务状态管理都塞进指令。及时清理副作用。在mounted中注册的事件监听器通常需要在unmounted中移除。考虑绑定值更新。如果指令依赖的绑定值会变化需要根据场景在updated等钩子中同步行为。避免不必要的直接 DOM 操作。如果通过 Vue 的响应式状态和模板就能清楚表达需求通常应优先使用 Vue 的声明式写法。十、课后练习与参考答案练习 1自动聚焦实现一个v-focus指令要求元素挂载后自动获得焦点。参考答案constvFocus{mounted(el){el.focus()}}使用方式input v-focus /练习 2禁止重复点击实现一个v-throttle-click指令让按钮点击后在指定时间内不能再次触发点击事件。参考答案思路指令在mounted时为元素添加点击监听器并记录上次触发时间在unmounted时移除监听器。下面是一个简单示例constclickHandlersnewWeakMap()constvThrottleClick{mounted(el,binding){constdelayNumber(binding.arg)||1000letlockedfalseconsthandler(event){if(locked)returnlockedtruebinding.value(event)setTimeout((){lockedfalse},delay)}clickHandlers.set(el,handler)el.addEventListener(click,handler)},unmounted(el){consthandlerclickHandlers.get(el)if(handler){el.removeEventListener(click,handler)clickHandlers.delete(el)}}}使用方式button v-throttle-click:1000submitForm 提交 /button这里的1000表示锁定时间为 1000 毫秒。该示例用于理解指令的结构实际项目应进一步考虑绑定函数更新、异常时解锁、组件卸载时定时器清理以及“节流”和“防重复提交”并不完全相同等问题。对于真正的提交防重通常还应在业务逻辑层设置提交状态不能只依赖 DOM 指令。练习 3权限控制实现一个v-permission指令传入当前用户权限和所需权限不满足条件时移除对应元素。参考答案思路简单的 DOM 移除示例constvPermission{mounted(el,binding){const{userPermissions,requiredPermission}binding.valueif(!userPermissions.includes(requiredPermission)){el.remove()}}}使用方式button v-permission{ userPermissions: permissions, requiredPermission: user:delete } 删除用户 /button需要注意el.remove()会把元素从 DOM 中移除。如果权限状态可能在页面运行期间变化单纯在mounted中判断并不够还需要设计更新处理或改用响应式模板条件。前端权限控制只影响界面展示不是后端授权的替代品。后端仍必须校验用户是否有执行操作的权限。练习 4方案选择判断下面的需求更适合组件、Composable还是自定义指令并说明原因。A. 封装一个支持搜索、分页和排序的表格。B. 封装一套可以在多个页面复用的搜索请求逻辑。C. 封装一个点击元素外部就关闭的行为。参考答案A优先使用组件因为它需要封装 UI 结构和交互如果搜索、分页状态逻辑也很复杂可以进一步抽到 Composable。B优先使用 Composable因为核心是复用响应式状态和请求逻辑而不是 UI。C可以使用自定义指令封装 DOM 事件行为如果关闭逻辑只服务于某个组件也可以直接写在该组件内部不必为了抽象而抽象。十一、本节核心面试题1. 什么是 Vue 3 自定义指令它主要解决什么问题参考答案自定义指令是 Vue 提供的一种封装 DOM 层面行为的机制适合处理需要直接操作原生 DOM 的场景。常见用途包括输入框自动聚焦。点击元素外部时关闭面板。添加或移除 DOM 事件监听器。动态设置原生 DOM 属性。例如script setup const vFocus { mounted(el) { el.focus() } } /script template input v-focus / /template这里的el就是绑定指令的原生 DOM 元素mounted钩子在元素挂载后执行因此可以调用focus()。需要注意自定义指令不等于添加事件监听器。自动聚焦和设置 DOM 属性也可以通过自定义指令实现。2. 组件、Composable 和自定义指令有什么区别参考答案三者的主要区别在于封装对象不同。方案主要职责典型场景Component封装 UI 结构与交互弹窗、表格、表单Composable封装响应式状态与业务逻辑分页、请求、搜索Custom Directive封装 DOM 行为自动聚焦、点击外部关闭选择时可以问自己三个问题是否需要封装一套 UI 结构和交互优先考虑组件。是否需要复用响应式状态或业务逻辑优先考虑 Composable。是否需要复用直接操作 DOM 的行为可以考虑自定义指令。例如复杂表格适合使用组件搜索请求逻辑适合使用 Composable而点击面板外部时关闭面板可以考虑使用自定义指令。如果某个 DOM 行为只服务于一个简单组件直接写在组件中也完全合理。3. Vue 3 自定义指令有哪些生命周期钩子最常用的是哪些参考答案Vue 3 自定义指令提供以下生命周期钩子钩子触发时机created元素属性和事件监听器等初始化之前beforeMount元素挂载到 DOM 之前mounted元素挂载到 DOM 之后beforeUpdate元素所在组件更新之前updated元素所在组件更新之后beforeUnmount元素卸载之前unmounted元素卸载之后最常用的是mounted初始化 DOM 行为例如自动聚焦、注册事件监听器。updated当指令依赖的绑定值变化时根据需要同步更新 DOM 行为。unmounted清理副作用例如移除事件监听器。例如指令在mounted中添加了全局事件监听器通常就应该在unmounted中移除。4. 自定义指令中的el和binding分别是什么参考答案el是绑定指令的原生 DOM 元素binding则包含指令的绑定信息。常见的binding属性包括属性含义binding.value当前绑定值binding.oldValue更新前的绑定值binding.arg指令参数binding.modifiers指令修饰符集合binding.instance使用该指令的组件实例例如input v-example:foo.barmessage /对应的信息是binding.arg// foobinding.modifiers// { bar: true }binding.value// message 当前对应的值指令也可以通过binding.value接收组件传入的函数div v-click-outsideclosePanel 面板内容 /div当检测到点击发生在元素外部时指令就可以调用binding.value()执行组件提供的关闭逻辑。5. 如何实现一个v-click-outside指令为什么必须清理事件监听器参考答案可以在元素挂载时监听document的点击事件判断点击目标是否位于当前元素之外如果在外部就调用组件传入的函数。consthandlersnewWeakMap()constvClickOutside{mounted(el,binding){consthandler(event){if(!el.contains(event.target)){binding.value()}}handlers.set(el,handler)document.addEventListener(click,handler)},unmounted(el){consthandlerhandlers.get(el)if(handler){document.removeEventListener(click,handler)handlers.delete(el)}}}使用方式div v-click-outsideclosePanel 面板内容 /div其中document.addEventListener()监听文档点击事件。el.contains(event.target)判断点击目标是否在当前元素内部。binding.value()调用组件传入的回调函数。document.removeEventListener()移除监听器。使用WeakMap可以保存 DOM 元素与事件处理函数的对应关系不必额外向 DOM 元素添加自定义属性。必须清理监听器的原因如果元素已经卸载但全局监听器仍然存在就可能继续执行无效逻辑造成资源浪费或意外行为。实际项目中还需要考虑绑定回调更新、点击触发按钮与面板关闭之间的事件顺序以及键盘操作和无障碍访问等问题。6. 如何实现一个会随绑定值变化而更新的自定义指令参考答案如果指令绑定的值可能发生变化就可以根据需求在updated钩子中同步 DOM 属性或行为。例如限制输入框的最大长度script setup const vMaxLength { mounted(el, binding) { el.maxLength binding.value }, updated(el, binding) { el.maxLength binding.value } } /script template input v-max-lengthmaxLength / /template这里mounted初始化maxLength。updated组件更新后将最新的绑定值同步到 DOM。如果绑定值从20变为10指令就可以将输入框的最大长度更新为10。不过并不是所有指令都需要实现updated。如果指令的行为只需要初始化一次例如简单的自动聚焦就可以只使用mounted。7. 自定义指令应该如何注册局部注册和全局注册有什么区别参考答案Vue 3 自定义指令有局部注册和全局注册两种常见方式。局部注册在script setup中按照vXxx的命名形式定义指令script setup const vFocus { mounted(el) { el.focus() } } /script template input v-focus / /template全局注册在应用初始化时使用app.directive()import{createApp}fromvueimportAppfrom./App.vueconstappcreateApp(App)app.directive(focus,{mounted(el){el.focus()}})app.mount(#app)之后组件就可以直接使用v-focus。两种方式的主要区别是使用范围局部注册适合只在特定组件中使用的指令。全局注册适合确实需要在多个页面或组件中复用的指令。不必将所有指令都注册为全局指令以免增加全局维护成本。8. 自定义指令有哪些设计原则和常见误区参考答案设计自定义指令时需要注意以下几点第一优先使用 Vue 原生能力。如果通过v-if、v-show、v-model或组件属性就能清晰地实现需求就不必强行使用自定义指令。第二指令主要负责 DOM 行为。不应把复杂的业务流程、接口请求和业务状态管理全部塞进指令。第三及时清理副作用。例如在mounted中注册的事件监听器通常需要在unmounted中移除。第四考虑绑定值更新。如果指令依赖的值会发生变化需要根据场景在updated等钩子中同步更新行为。第五避免不必要的直接 DOM 操作。如果通过 Vue 的响应式状态和模板就能完成需求通常应优先使用声明式写法。核心原则是自定义指令应解决明确的 DOM 行为复用问题而不是替代组件、Composable 或 Vue 的响应式机制。9. 如何实现一个简单的防重复点击指令为什么不能只依赖指令防止重复提交参考答案可以通过自定义指令监听按钮点击事件并在指定时间内阻止重复触发。例如constclickHandlersnewWeakMap()constvThrottleClick{mounted(el,binding){constdelayNumber(binding.arg)||1000letlockedfalseconsthandler(event){if(locked)returnlockedtruetry{binding.value(event)}finally{setTimeout((){lockedfalse},delay)}}clickHandlers.set(el,handler)el.addEventListener(click,handler)},unmounted(el){consthandlerclickHandlers.get(el)if(handler){el.removeEventListener(click,handler)clickHandlers.delete(el)}}}使用方式button v-throttle-click:1000submitForm 提交 /button这里的1000表示锁定时间为 1000 毫秒。不过这只是简化示例。实际项目还应处理定时器清理、绑定函数更新等问题并明确区分节流和防重复提交。不能只依赖指令防止重复提交因为定时锁定只是限制一段时间内的点击行为并不一定与异步请求的真实状态一致。对于表单提交更稳妥的方式通常是在业务逻辑中维护submitting状态并在请求完成后恢复状态。真正的数据安全仍应由后端根据业务规则保障。10. 权限控制适合用自定义指令实现吗有哪些注意事项参考答案可以使用自定义指令实现简单的 DOM 展示控制但需要结合需求判断是否合适。例如constvPermission{mounted(el,binding){const{userPermissions,requiredPermission}binding.valueif(!userPermissions.includes(requiredPermission)){el.remove()}}}使用方式button v-permission{ userPermissions: permissions, requiredPermission: user:delete } 删除用户 /button如果用户没有对应权限指令就会移除该元素。但需要注意el.remove()会直接从 DOM 中移除元素。如果权限状态可能变化只在mounted中判断是不够的还需要设计更新处理。如果使用响应式模板条件或权限组件就能清晰实现需求通常应优先考虑这些方案。前端权限控制只能控制界面展示不能替代后端授权校验。即使按钮在前端被隐藏后端仍然必须验证当前用户是否有权执行对应操作。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑