Vue3:根据el-input封装全局v-focus指令
问:
/*** v-focus* 按钮权限指令*/
import type { Directive } from 'vue'
import { nextTick } from 'vue'const vFocus: Directive = {async mounted(el: HTMLElement) {setTimeout(() => {const input = el.querySelector<HTMLInputElement>('.el-input__inner') || el.querySelector('input') || el// 微任务延迟确保渲染完成if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {input.focus() // 直接聚焦找到的元素// 更新ElementPlus输入框样式const wrapper = input.closest('.el-input')wrapper?.classList.add('is-focus')}}, 200)},
}export default vFocus
请问这段代码是我封装的自定义v-focus方法永爱自动触发el-input的输入框的光标的,但是现在进入页面只能触发一次,关闭弹框后再次打开弹框el-input的光标就不会自动触发了,请问怎么回事怎么解决
回答:
问题复现
- 页面第一次打开弹框,
v-focus
能正常把el-input
(或普通<input>
)聚焦并给外层.el-input
加上is-focus
样式。- 关闭弹框(弹框的 DOM 被
v-if
或v-show
隐藏/销毁)后再次打开,光标不再自动出现。这通常是因为 指令只在元素第一次挂载 (
mounted
) 时执行,而弹框关闭后 Vue 并没有重新触发mounted
(如果是v-show
只切换display
,DOM 并未销毁),或者弹框是通过v-if
销毁后又重新创建,但 指令的mounted
回调中的setTimeout
已经在第一次挂载后就被清理掉,导致后续实例没有再执行聚焦逻辑。
1. 了解指令的生命周期钩子
钩子 何时调用 created
指令第一次绑定到元素上(还未挂载到 DOM) beforeMount
元素即将挂载到 DOM( mounted
之前)mounted
元素挂载到 DOM, el
已经可用beforeUpdate
VNode 更新前(属性/子节点可能变化) updated
VNode 更新后,DOM 已经重新渲染 beforeUnmount
指令即将被卸载(元素即将从 DOM 移除) unmounted
指令已经被卸载(元素已从 DOM 中移除) 关键点:如果弹框是通过
v-show
隐藏/显示,元素始终保持挂载状态,只会触发updated
(因为show
/hide
会导致属性变化)。如果是v-if
,每次打开都会重新创建元素,mounted
会再次执行。
2. 为什么只能聚焦一次?
场景 A:使用
v-show
隐藏弹框
v-show
只改动display
,不会触发mounted
。- 你的指令只在
mounted
时聚焦一次,弹框再次显示时指令不再运行,导致光标不出现。场景 B:使用
v-if
,但指令内部的setTimeout
被缓存
v-if
销毁后再次创建,mounted
会再次执行。- 但是
setTimeout
的延迟(200 ms)可能在弹框内部的动画或nextTick
完成前就执行,导致聚焦的目标元素仍未渲染完毕,从而失效。- 另外,如果弹框内部还有
el-input
的v-model
异步更新,nextTick
需要再等一次微任务。
3. 推荐的解决方案
下面提供 三种可行的实现,任选其一即可。
(1)把聚焦逻辑放到updated
/beforeUpdate
,配合nextTick
确保 DOM 已经渲染完;
(2)在指令里监听弹框的 打开事件(如@open
、@after-enter
);
(3)使用v-focus
+v-if
的组合,确保每次弹框创建时都走mounted
。3.1 最简版:在
mounted
+updated
中都执行一次import type { Directive } from 'vue' import { nextTick } from 'vue'function focusInput(el: HTMLElement) {// 1. 先找最可能的输入框const input =el.querySelector<HTMLInputElement>('.el-input__inner') ||el.querySelector('input') ||el// 2. 必须是可聚焦的表单控件if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return// 3. 使用 nextTick 确保所有子组件已渲染nextTick(() => {input.focus()const wrapper = input.closest('.el-input')wrapper?.classList.add('is-focus')}) }const vFocus: Directive = {// 第一次挂载(适用于 v-if 重建的情况)mounted(el) {// 加一点延迟防止弹框动画未完成setTimeout(() => focusInput(el), 200)},// 当父组件重新渲染(v-show 切换 display)时再次尝试聚焦// updated 会在每次 VNode 更新后触发updated(el) {// 这里不需要再加 setTimeout,直接 nextTick 即可focusInput(el)},// 可选:在指令被销毁时清理可能残留的 classunmounted(el) {const input = el.querySelector('input')const wrapper = input?.closest('.el-input')wrapper?.classList.remove('is-focus')}, }export default vFocus
为什么可行?
mounted
负责 首次(v-if
)的聚焦,保留原来的 200 ms 延迟以兼容弹框动画。updated
会在 每次弹框显示/隐藏(v-show
)或内部状态变化时触发,内部使用nextTick
等待子组件渲染完毕,再聚焦。- 这样无论是
v-show
还是v-if
,指令都会在合适的时机再次执行聚焦逻辑。
3.2 进阶版:监听弹框的
after-enter
(如果使用 Element PlusElDialog
)如果你使用的是 Element Plus 的
ElDialog
(或者其他基于transition
的弹框),可以在指令内部监听弹框的transitionend
/after-enter
事件,确保动画结束后再聚焦:const vFocus: Directive = {mounted(el) {const dialog = el.closest('.el-dialog')if (dialog) {// 当弹框打开动画结束后触发const onAfterEnter = () => {focusInput(el)dialog?.removeEventListener('after-enter', onAfterEnter)}dialog.addEventListener('after-enter', onAfterEnter)} else {// 非弹框场景,直接聚焦setTimeout(() => focusInput(el), 200)}},// 同样保留 updated 以防 v-show 场景updated(el) {focusInput(el)}, }
注意:
ElDialog
在内部会把after-enter
事件挂在根元素(.el-dialog__wrapper
),因此需要closest('.el-dialog')
或者直接在弹框组件上绑定@after-enter="..."
并在外层调用focus()
。
3.3 另一种思路:使用
nextTick
+requestAnimationFrame
有时候弹框的渲染顺序非常快,
setTimeout(200)
可能仍然早于真实可聚焦的时机。可以把 两层异步组合起来:function focusWhenReady(el: HTMLElement) {nextTick(() => {requestAnimationFrame(() => {const input = /* 同上 */ if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {input.focus()input.closest('.el-input')?.classList.add('is-focus')}})}) }
nextTick
→ 微任务,随后requestAnimationFrame
→ 下一帧渲染,基本可以保证输入框已经真正出现在页面上。
4. 小技巧:防止重复添加
is-focus
如果弹框打开多次,
is-focus
类可能会一直残留在上一次的 DOM 节点上,导致样式不统一。可以在聚焦前先 移除 再 添加:const wrapper = input.closest('.el-input') wrapper?.classList.remove('is-focus') wrapper?.classList.add('is-focus')
或者在
unmounted
/beforeUnmount
时统一清理。
5. 完整示例(整合所有建议)
import type { Directive } from 'vue' import { nextTick } from 'vue'function focusInput(el: HTMLElement) {const input =el.querySelector<HTMLInputElement>('.el-input__inner') ||el.querySelector('input') ||elif (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) returnnextTick(() => {// 让浏览器在下一帧真正渲染后再聚焦requestAnimationFrame(() => {input.focus()const wrapper = input.closest('.el-input')wrapper?.classList.remove('is-focus')wrapper?.classList.add('is-focus')})}) }const vFocus: Directive = {mounted(el) {// 如果是弹框组件,等弹框动画结束再聚焦const dialog = el.closest('.el-dialog')if (dialog) {const handler = () => {focusInput(el)dialog.removeEventListener('after-enter', handler)}dialog.addEventListener('after-enter', handler)} else {// 普通场景保持原来的延迟setTimeout(() => focusInput(el), 200)}},// 兼容 v-show 切换或内部数据变化updated(el) {focusInput(el)},// 清理样式,防止残留unmounted(el) {const input = el.querySelector('input')const wrapper = input?.closest('.el-input')wrapper?.classList.remove('is-focus')}, }export default vFocus
使用方式
<template><el-dialog v-model="show" title="编辑"><el-input v-focus v-model="name" placeholder="请输入姓名" /></el-dialog> </template><script setup lang="ts"> import { ref } from 'vue' import vFocus from '@/directives/v-focus'const show = ref(false) const name = ref('') </script>
6. 小结
关键点 说明 指令只在 mounted
执行一次对 v-show
隐藏的弹框不起作用使用 updated
(或beforeUpdate
)每次弹框显示/内容变化时重新聚焦 配合 nextTick
+requestAnimationFrame
确保 DOM 已完成渲染后才调用 .focus()
如果有弹框动画 监听弹框的 after-enter
/transitionend
再聚焦清理 is-focus
防止类名残留导致样式异常 把上述任意一种实现方式替换掉你原来的
vFocus
,就可以在弹框每次打开时都自动聚焦el-input
,不会再出现“只能触发一次”的情况。祝开发愉快 🎉!