解决 Element Plus 中 el-space 与 v-if/v-show 的渲染冲突问题
前言
最近在开发后台管理系统时,我遇到了一个常见的需求:在表格的操作列中,根据数据状态动态显示不同的操作按钮。
为了让按钮之间排列整齐并带有分隔符,我使用了 Element Plus 的 el-space 组件。代码如下:
<el-space size="16" spacer="|">
<el-button type="primary" text> 修改 </el-button>
<el-button v-if="isRecycle" type="success" text> 还原 </el-button>
<el-button type="danger" text > 删除 </el-button>
</el-space>预期效果是:当 isRecycle 为 false 时,“还原”按钮不渲染,并且“修改”和“删除”按钮之间只有一个分隔符。
但实际情况是,即便 isRecycle 为 false,“还原”按钮确实消失了,但它原本所在的位置却留下了一个多余的分隔符。

研究了一上午,终于是找到了解决办法。
问题分析:为何会产生多余的分隔符?
要解决问题,首先需要理解 el-space 的工作原理和 v-if 在 Vue 3 中的行为。
el-space的工作原理:
通过阅读源码可以发现,el-space组件的核心逻辑是遍历其默认插槽(default slot)中的所有 VNode (虚拟节点)。然后,在除了最后一个有效节点之外的每个节点后面,插入一个spacer元素(即我们设置的|分隔符)。v-if="false"的行为:
在 Vue 3 中,当一个元素或组件的v-if指令为false时,它不会被渲染到真实 DOM 中。然而,在虚拟 DOM 层面,它会被渲染成一个注释节点(Comment Node),作为占位符。
结论:当 v-if="false" 时,el-space 获取到的插槽内容 VNode 列表实际上是 [VNode(修改按钮), VNode(注释节点), VNode(删除按钮)]。el-space 将这个注释节点视为一个有效的子节点,因此在“修改按钮”和“注释节点”后面都添加了分隔符,导致最终渲染出两个分隔符,而第二个分隔符因为“注释节点”本身不可见,看起来就像是多余的。
解决方案
既然问题根源在于 el-space 将注释节点也计入了分隔符的渲染逻辑,那么我们的目标就是避免让注释节点出现在 el-space 的直接子节点中。
我封装了一个组件:TableOperations.vue
实现方式如下,通过组合式 API useSlots 和计算属性来动态生成一个“干净”的子节点列表:
<script lang="ts">
import { ElSpace } from "element-plus";
import type { SpaceProps } from "element-plus";
import { Comment } from "vue";
export default defineComponent({
name: "TableOperations",
setup(props) {
const slots = useSlots();
return () =>
h(
ElSpace,
{
size: 0,
spacer: "|"
},
() => (slots.default?.() || []).filter((node) => node?.type !== Comment)
);
}
});
</script>
现在v-if的问题解决了,但是在真实的业务场景中,我们可能还需要考虑其他因素,比如:v-show,有时候为了减少渲染开销,我们可能需要使用 v-show 来代替 v-if。
但是上面的代码只兼容了v-if,对于v-show的情况,我们需要进一步处理。
<script lang="ts">
import { ElSpace } from "element-plus";
import type { SpaceProps } from "element-plus";
import { Comment, type VNode } from "vue";
export interface TableOperationsProps {
size?: SpaceProps["size"];
spacer?: SpaceProps["spacer"];
}
export default defineComponent({
name: "TableOperations",
props: {
size: {
type: [Number, String] as PropType<SpaceProps["size"]>,
default: "small"
},
spacer: {
type: [String, Object] as PropType<SpaceProps["spacer"]>
}
},
setup(props) {
const slots = useSlots();
/** 判断是不是Comment节点
* 当`v-if="false"`时默认会插入一个注释节点Comment
*/
function isComment(node: VNode): boolean {
return node?.type === Comment;
}
/** 判断是不是v-show指令 */
function isVShowDirective(dir: DirectiveBinding): boolean {
// @ts-expect-error fuck ts
return typeof dir.dir.name === "string" && dir.dir.name === "show";
}
return () =>
h(
ElSpace,
{
size: props.size,
spacer: props.spacer
},
() => {
return (slots.default?.() || []).filter((node) => {
// 剔除 v-if="false" (Comment 节点)
if (isComment(node)) return false;
// 检查 v-show="false"
if (node.dirs && node.dirs.length) {
const findShow = node.dirs.find(isVShowDirective);
if (findShow) return Boolean(findShow.value);
}
// 既不是 v-if=false 也不是 v-show=false
return true;
});
}
);
}
});
</script>
做法也很粗暴,在剔除了注释节点后,再来获取 v-show 指令,并根据其值来决定是否渲染该节点。
完善功能
目前组件已经达到了我们需要的功能,但是还有一些不足,就是我们没办法完全将 TableOperations 作为一个增强版的 ElSpace 组件来使用,比如它不支持其它的props属性,但是事实上我们只是在 ElSpace 组件的基础上对插槽(slot)做了增强,所以,为了不仅仅是在表格中使用它,在别的地方也可以使用它,我们还需要完善属性传递,使其可以像正常的 ElSpace 组件一样使用。
<script lang="ts">
import { ElSpace } from "element-plus";
import { type SpaceProps, spaceProps } from "element-plus";
import { Comment, type VNode } from "vue";
export type ElSpaceProProps = SpaceProps;
export default defineComponent({
name: "ElSpacePro",
props: spaceProps,
setup(props) {
const slots = useSlots();
const attrs = useAttrs();
/** 判断是不是Comment节点
* 当`v-if="false"`时默认会插入一个注释节点Comment
*/
function isComment(node: VNode): boolean {
return node?.type === Comment;
}
/** 判断是不是v-show指令 */
function isVShowDirective(dir: DirectiveBinding): boolean {
// @ts-expect-error fuck ts
return typeof dir.dir.name === "string" && dir.dir.name === "show";
}
return () =>
h(
ElSpace,
{
...props,
...attrs
},
() => {
return (slots.default?.() || []).filter((node) => {
// 剔除 v-if="false" (Comment 节点)
if (isComment(node)) return false;
// 检查 v-show="false"
if (node.dirs && node.dirs.length) {
const findShow = node.dirs.find(isVShowDirective);
if (findShow) return Boolean(findShow.value);
}
// 既不是 v-if=false 也不是 v-show=false
return true;
});
}
);
}
});
</script>做法也很简单,把props定义成ElSpace的props就行了,然后传递过去,并且组件名我们也改成 ElSpacePro 使其泛用性更高。
效果展示

实现原理解析
- 获取插槽内容:通过 Vue 3 提供的
useSlots()hook 访问组件插槽。slots.default()的执行结果是一个包含所有默认插槽子节点的 VNode 数组。 - 使用render组件函数:使用render组件函数可以实现更灵活的渲染逻辑,我测试了使用template的方式,会导致节点的diff发生问题,导致绑定的数据无法更新。
过滤插槽内容 (核心): 遍历
slots.default()数组,过滤掉Comment类型的节点,在vue3中,v-if="false"在 VDOM 中会生成一个注释节点(Comment Node)。这类特殊节点的类型由 Vue 内部的一个Symbol` 标识。
最健壮且官方推荐的识别方法,是使用 Vue 自身导出的类型标识符。代表注释节点的Symbol被导出为Comment。因此,我们可以从'vue'中导入它,并使用node.type !== Comment作为精确的过滤条件。这种方式避免了依赖内部实现细节的脆弱判断(如
node.type.toString() === 'Symbol(v-cmt)'),保证了代码在 Vue 版本迭代中的稳定性和向后兼容性。- 渲染最终结果:通过
h(ElSpace)调用ElSpace组件,并传入size和spacer属性,并使用() => (slots.default?.() || []).filter((node) => node?.type !== Comment)作为插槽内容,注意插槽内容是一个函数,不然会触发vue的警告。
总结
el-space 与 v-if 的渲染冲突,本质上是 el-space 的通用分隔符逻辑与 Vue 3 v-if="false" 产生的注释节点之间的不兼容。通过手动介入 VNode 的处理流程,即使用 useSlots 获取插槽内容,并过滤掉 Comment 类型的节点,我们可以在不改变原有业务逻辑和模板结构的前提下,优雅地解决这一问题,确保最终渲染效果符合预期。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据