--- title: 【SearchPanel组件】下拉联想结果的全局监听click事件技术实现 description: 写SearchPanel组件时实现下拉联想结果点击外部自动关闭,同时用了全局click事件监听来代替常用的onBlurSuggest逻辑 date: 2025-02-14T01:31:00+08:00 slug: 下拉联想 categories: - 实习小记 - 奇技淫巧 tags: [ "Vue", "前端", "JavaScript" ] lastmod: 2025-02-14T02:58:00+08:00 links: - title: 【SearchPanel组件】详细配置文档 description: SearchPanel是一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,支持响应式数据更新 website: /post/实习小记/searchpanel组件文档/ --- 给公司写Vue组件时,需要实现一个下拉联想结果,这个下拉联想结果是根据用户输入内容实时调用API获取的,当用户输入内容时,下拉联想结果会自动弹出。自然地,如果用户点击了其他地方,下拉联想结果应该自动关闭,以保证用户体验。 其实一开始我用的是常用的`onBlurSuggest`实现,但是发现有时候点击外部没办法正确隐藏联想结果,分析了一下发现`blur`事件可能在`focus`事件之后触发,所以无法实现点击外部自动关闭。 所以我换了种方案,对于输入框和联想结果,我分别加了`ref`来标识两个组件,方便后面检查点击事件时进行定位。输入框: ```html ``` 联想结果: ```html
``` 两个 ref 属性的用来建立 DOM 元素与组件实例的关联。然后我们在`methods`中定义了`handleClickOutside`方法,用来处理点击外部事件。我们遍历所有的联想输入框,检查点击事件是否发生在输入框或下拉联想结果的外部,如果是,就关闭下拉联想结果。在方法中,我们通过`this.$refs`访问到对应的 DOM 元素。 ```js handleClickOutside(event) { // 遍历所有联想输入框 this.fields.forEach(field => { if (field.type === 'suggest') { const dropdownRef = this.$refs['dropdown-' + field.key] // 跟踪联想结果 const inputRef = this.$refs['input-' + field.key] // 跟踪输入框 // 获取实际 DOM 元素 const dropdownEl = dropdownRef ? dropdownRef[0] : null const inputEl = inputRef ? inputRef[0] : null // 检查点击是否在下拉框或输入框内 const clickedInDropdown = dropdownEl && dropdownEl.contains(event.target) const clickedInInput = inputEl && inputEl.contains(event.target) if (!clickedInDropdown && !clickedInInput) { // 点击不在下拉框和对应的输入框内,关闭下拉框 this.$set(this.showDropdown, field.key, false) } } }) }, ``` 在`mounted`和`beforeDestroy`钩子中分别添加事件监听和移除。注意别忘了在组件销毁前,要移除全局的点击事件监听器,避免内存泄漏。 ```js mounted() { document.addEventListener('click', this.handleClickOutside) // ... }, beforeDestroy() { document.removeEventListener('click', this.handleClickOutside) }, ``` 这样一来,可以精确地保证点击外部任意位置时,联想结果都会自动关闭。 那么再来说说这样全局监听`click`的方法相比`onBlurSuggest`的优点吧,首先我刚刚说了,通过全局`click`事件监听,可以判断点击事件是否发生在输入框或下拉列表内部,比简单的`blur`事件更灵活,能更好地处理复杂布局或嵌套组件的情况。(事实上,我前面用`blur`的时候就发现,有时候点击外部没办法正确隐藏联想结果) 比如: ```html
``` 这种情况下全局方案可以识别按钮的点击,但是`blur`方案会误判为离开输入区域。另一个例子(跨组件): ```html ``` 使用`blur`事件时,我们依赖的是输入框与下拉框之间的“焦点”关系来判断用户是否点击在下拉菜单外部。但是通过 Teleport 将下拉菜单渲染到 body 时,输入框和下拉菜单就不再处于同一 DOM 层级内。由于下拉菜单被 Teleport 到了 body,它与输入框在 DOM 树中完全分离。点击下拉菜单,输入框会失去焦点,从而触发 `blur` 事件,而这时我们其实希望下拉菜单保持显示状态。反之,如果不触发 `blur`,又无法区分点击是否发生在下拉菜单上,所以单纯依赖 `blur` 很难准确判断点击位置。 换句话说,`blur`事件本质上只是反映了焦点丢失,而不能判断新获得焦点的元素是否属于下拉菜单的范围。对于 Teleport 渲染的结构,浏览器没办法将下拉菜单与输入框关联起来,从而导致`blur`事件失效,或者触发不符合预期。而全局的`click`事件方案则不依赖于组件之间的DOM层级关系。它是直接监听整个文档的点击事件,然后通过`contains`方法判断点击是否发生在输入框或下拉菜单内。无论下拉菜单渲染在哪,只要能获取到DOM引用(`ref`),全局方案都能正确判断、控制下拉菜单的显示和隐藏。 另一方面,时序上,用户点击下拉建议时,输入框会先触发`blur`失焦,导致下拉框提前关闭,就可能会阻止`click`或`mousedown`事件在下拉项上被正确触发。而全局`click`事件判断可以确保在点击事件发生后再做判断,避免这种“先失焦后点击”带来的问题。 | 方案 | 事件触发顺序 | 典型问题场景 | |---------------------|----------------------------------|----------------------------------------------------------------------------| | **blur 方案** | `mousedown` -> `blur` -> `click` | 点击下拉选项时,输入框先触发 `blur` 导致下拉关闭,无法触发选项的 `click` 事件,一般要设置延时(200ms左右) | | **全局点击方案** | 直接捕获 `click` 事件 | 可通过 `mousedown` 提前处理选择逻辑,完美解决时序问题 | 另外,页面中有多个联想输入框时,使用全局`click`事件就能统一管理所有输入框的失焦逻辑,避免为每个输入框单独绑定`blur`事件可能带来的冗余代码,或者一些其他的不一致状态。 又熬到3点了,看来这辈子不可能早睡了。。。附组件的HTML和JS部分完整代码:(此组件已修改,下面为修改前的代码,非最终版本) ```html ```