问:
/**
* 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) }, // 可选:在指令被销毁时清理可能残留的 class unmounted(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') || el if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return nextTick(() => { // 让浏览器在下一帧真正渲染后再聚焦 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
,不会再出现“只能触发一次”的情况。祝开发愉快 🎉!