问题现象
有一段代码,使用 element-ui(Vue2)的 popover 组件,popover 面板中有一个 select 下拉框,使用 click 来触发面板
<el-popover placement="bottom" :width="400" trigger="click">
<span>hello world</span>
<el-select v-model="value">
<el-option label="one" value="one"></el-option>
<el-option label="two" value="two"></el-option>
</el-select>
<div>
<el-button @click="okFn">确定</el-button>
<el-button @click="cancelFn">取消</el-button>
</div>
<template #reference>
<div>
<el-button>打开</el-button>
</div>
</template>
</el-popover>
把这段代码迁移到 Vue3 时,发现选中 select 下拉框的选项时,popover 面板就自动收起了。
根因分析
想知道展开下拉框时触发了什么代码,所以,对event listener打断点。
点击下拉选项,发现进入了 @vueuse+core@9.13.0_vue@3.4.24/node_modules/@vueuse/core/index.mjs 的 listener 函数,执行 handler 函数
handler 对应的代码在 element-plus@2.7.1_vue@3.4.24/node_modules/element-plus/es/components/tooltip/src/content2.mjs
点开 content2.mjs,发现进入了 onClose 函数
onClose 对应的代码在 tooltip2.mjs 中
点开 tooltip2.mjs,发现执行了 onClose 函数
onClose 代码在 index.mjs
进入函数,发现代码位置在 element-plus@2.7.1_vue@3.4.24/node_modules/element-plus/es/hooks/use-delayed-toggle/index.mjs
到这里可以看出:
element-plus的popover组件,依赖tooltip组件;- 当
tooltip的trigger的值不是hover时,会触发close事件; - 下拉框的
click事件,触发了tooltip组件的close事件
看看 Vue3 的 element-plus 的 popover 是如何实现的:
是通过 tooltip 组件实现的。
再对比 Vue2 的 element-ui,发现 Vue2 的 popover 是通过 vue-popper 实现的。
看看DOM是啥样的
可以看出,select的下拉框,是挂载在#popper-container中,和#app是同级的
再看看popover面板所在的DOM,是和select下拉框同级的
为啥两个同级的DOM,下拉框的click会触发popover(tooltip)的close事件呢?
回到一开始进入的回调函数,发现下方的函数中有一行代码:
点击下拉框时,会调用这行代码,所以 click 事件就绑定了回调:listener 函数。
总结一下,element 组件执行逻辑是:
点击下拉框,会绑定 click 的事件回调函数。
点击下拉选项,会执行 click 的回调函数:lisntener,以及内部的 handler 函数。handler 函数对应的代码在 tooltip 组件中。
执行 handler 时,由于tooltip 的 trigger 不为 hover,所以会执行 close 函数。这就导致了 popover 面板被关闭。
解决方案
原因定位出来了,如何解决呢?
还是看回这段代码:
composedPath表示调用该侦听器时返回事件路径 MDN 文档
当这个条件满足时,可以让函数提前返回,不执行handler函数
当前的composedPath是这样的:
可以看出,这是click事件冒泡时的DOM顺序。当 select 的下拉框在 popover 的内部时,click事件冒泡的 composedPath 会包含 popover,此时 event.composedPath().includes(el) 结果为true
所以,这个问题的解决办法就是:将 select 下拉框的 DOM 放到 popover 内部,可以通过修改 el-select组件的 teleported 属性为 false 来实现。
修改后,代码如下:
<el-popover placement="bottom" :width="400" trigger="click">
<span>hello world</span>
<el-select v-model="value" :teleported="false">
<el-option label="one" value="one"></el-option>
<el-option label="two" value="two"></el-option>
</el-select>
<div>
<el-button @click="okFn">确定</el-button>
<el-button @click="cancelFn">取消</el-button>
</div>
<template #reference>
<div>
<el-button>打开</el-button>
</div>
</template>
</el-popover>
断点执行结果如下:
event.composedPath 已经包含了el,listener函数提前返回了。
页面效果如下:
总结
Vue3的element-plus相对于Vue2的element-ui,popover组件的实现方式有较大变化:不使用vue-popper,而是直接用el-tooltip来实现。element-plus的popover组件内部如果使用了el-select组件,需要确保el-select的teleported值为false,否则会出现展开select下拉框时,popover组件自动收起。- 当遇到和浏览器事件有关的问题时,可以通过浏览器的
event listener来打断点,快速找到事件回调。