Vue.js嵌套事件执行顺序问题:搜索建议框点击失效的排查与解决
Vue.js嵌套事件执行顺序问题:搜索建议框点击失效的排查与解决
我太懂这种糟心的情况了——明明点了建议项,结果搜索框的焦点刚移开,建议框就先消失了,绑定的赋值逻辑根本没机会执行,还不想用setTimeout这种“凑活”的办法对吧?咱们来一步步拆解问题,找到优雅的解决方案。
问题根源:事件执行顺序踩坑
你猜的没错,核心就是事件触发的先后顺序搞的鬼:
当你点击建议项里的<span>时,浏览器的事件流是这样的:
- 鼠标从
<input>移到<span>,<input>失去焦点,触发容器的focusout事件,把show.value设为false - 这时候Vue会立即更新DOM,把
v-if="show"的建议框从页面上移除 - 后续的
click事件因为目标元素已经不存在,根本不会执行
而且nextTick没用的原因也很简单:它是等待当前事件循环的DOM更新完成后再执行,但focusout是同步触发的,show.value = false已经同步修改了,nextTick回调执行的时候,建议框早就没了。
解决方案1:用mousedown替换click(最简单)
浏览器的事件执行顺序里,mousedown是在focusout之前触发的!我们把建议项的click事件换成mousedown,就能先完成赋值,再处理焦点变化:
<script setup lang="ts"> import { ref } from 'vue' const show = ref(false) const model = ref('') const setFocus = (on: boolean) => { show.value = on } </script> <template> <div id="container" @focusin="setFocus(true)" @focusout="setFocus(false)"> <input class="input-field" type="text" placeholder="focus me" v-model="model" /> <div id="conditional" v-if="show"> <!-- 把@click换成@mousedown --> <span @mousedown="model = 'it works !'">click me</span> </div> </div> </template> <style scoped> .input-field { border: 1px solid #000; padding: 8px; border-radius: 8px; } #conditional { cursor: pointer; } </style>
这样一来,mousedown先触发,把model赋值完成,之后哪怕focusout隐藏了建议框,结果已经正确更新了。
解决方案2:判断焦点是否仍在容器内(更严谨)
如果不想改事件类型,我们可以在focusout触发时,检查新获得焦点的元素是否还在容器内部——如果是,说明只是容器内的焦点切换,不需要隐藏建议框:
<script setup lang="ts"> import { ref } from 'vue' const show = ref(false) const model = ref('') const setFocus = (on: boolean, event?: FocusEvent) => { if (!on) { // 获取容器元素 const container = document.getElementById('container') // 检查失去焦点后,新焦点是否在容器内 if (container && event?.relatedTarget && container.contains(event.relatedTarget as Node)) { return // 焦点还在容器里,不隐藏建议框 } } show.value = on } </script> <template> <div id="container" @focusin="setFocus(true)" @focusout="setFocus(false, $event)"> <input class="input-field" type="text" placeholder="focus me" v-model="model" /> <div id="conditional" v-if="show"> <span @click="model = 'it works !'">click me</span> </div> </div> </template> <style scoped> .input-field { border: 1px solid #000; padding: 8px; border-radius: 8px; } #conditional { cursor: pointer; } </style>
这里用到了FocusEvent的relatedTarget属性,它指向的是触发focusout时新获得焦点的元素。我们通过container.contains()判断这个元素是否在容器内,是的话就不执行隐藏逻辑,这样click事件就能正常触发了。
总结
两种方案都能避开setTimeout的hack:
- 方案1简单直接,适合大多数场景
- 方案2更严谨,适合复杂的容器内焦点交互场景
备注:内容来源于stack exchange,提问作者hwk
相关产品推荐
相关产品推荐

