el-popover在v-for 循环中的使用与优化

本文探讨了在Vue中使用el-popover组件在v-for循环中可能引发的性能问题,并提出了优化方案。通过将el-popover组件移出循环,利用reference属性和自定义事件来控制popover的显示和关闭,从而避免多次渲染,提升了首屏加载速度。详细解释了实现这一优化的代码逻辑和原理,并提供了实际的代码示例。同时,分析了优化后的性能效果,展示了首屏渲染时间的显著改善。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

el-popover 在 v-for 中的 基础使用

<div v-for='item in 100'>
    <el-popover
        placement="bottom"
        title="标题"
        width="200"
        trigger="click"
        content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
        <el-button slot="reference">click 激活</el-button>
    </el-popover>
</div>

这样使用的缺点是pop-over内嵌的内容回多次重复渲染,在数据量不大的时候影响不大,如果pop-over嵌套table等其他组件会导致效率低下,多次重复渲染。当然为了完成需求可以首先使用这种方法

如何优化

优化思路就是将el-popover提出来,不参与循环,让el-popover只渲染一次,这样在首屏渲染时,速度就会大大提升。这样就有两个问题需要解决:

  1. 如何将popover slot中的reference与for循环中的button关联起来,用来确定popover的出现位置。
  2. 如何触发popover显示与关闭

el-popover有几种激活方式,分click与v-model等。

  1. click模式下,需要将button作为reference slot,只有点击reference才会显示popover,点击非popover区域会自动关闭popover,但是我们需要将popover提出来,而reference在v-for中渲染,自然不可能使用slot。
  2. 使用v-model时可以很方便控制显示与关闭但是点击非popover区域无法自动关闭。

查看el-popover 源代码寻找解决办法

element-ui 2.15 其他版本自行查看代码是否一致
props: {
    // 有reference prop 但是官方文档未写这个参数
    reference: {}
}


mounted()
{
    // 这块是确定 reference的逻辑 首先获取props参数 没有就获取refs,所以我们可以将refence当作props传递给el-popover组件
    let reference = this.referenceElm = this.reference || this.$refs.reference;
    ...
    // 可以看到当trigger为click时监听了点击事件 当点击不是popover的区域时自动隐藏popover以及点击reference自身进行toggle操作
    if (this.trigger === 'click') {
        on(reference, 'click', this.doToggle);
        on(document, 'click', this.handleDocumentClick);
    }
}

methods: {
    // 这几个方法主要是控制popover 显示与隐藏的 我们可以使用这几个方法
    doToggle()
    {
        this.showPopper = !this.showPopper;
    },
    doShow()
    {
        this.showPopper = true;
    },
    doClose()
    {
        this.showPopper = false;
    }
}
确定思路
  1. 使用reference prop将v-for中的button当作参数传递给el-popover,这样就可以不使用slot而将reference与el-popover分离开了
  2. trigger使用click实现点击其他区域自动隐藏popover以及点击reference进行toggle操作
talk is cheap,show you the code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link href="https://www.unpkg.com/element-ui@2.15.3/lib/theme-chalk/index.css" rel="stylesheet">
</head>
<body>
<script src="https://www.unpkg.com/vue/dist/vue.js"></script>
<script src="https://www.unpkg.com/element-ui@2.15.3/lib/index.js"></script>
<div id="app">
</div>
<template id="test">
    <div>
        <div v-for='item in 100'>
            <el-button @click='clickPop(item)' :ref='`bt`+item'>click 激活</el-button>
        </div>
        <el-popover
                v-if='showPop'
                ref='pop'
                :reference='reference'
                placement="bottom"
                title="标题"
                width="200"
                trigger="click"
        >
            <el-button @click='$refs.pop.doClose()'>自定义关闭按钮</el-button>
        </el-popover>
    </div>
</template>
<script>
    var Main = {
        template:'#test',
        beforeCreate(){
            console.time('per')
        },
        mounted(){
            let time = console.timeEnd('per')
        },
        data() {
            return {
                reference:{},
                // 控制渲染条件 如果不用v-if会报错 具体可自行尝试
                showPop: false,
                // 保存当前激活的refrence id
                activeId:'',
            };
        },
        methods:{
            clickPop(item){
                // 这个操作是为了避免与源码中的点击reference doToggle方法冲突
                if (this.activeId === item && this.showPop) return
                this.showPop = false
                this.activeId = item
                // 因为reference是需要获取dom的引用 所以需要是$el
                this.reference = this.$refs['bt'+item][0].$el
                this.$nextTick(() => {
                    // 等待显示的popover销毁后再 重新渲染新的popover
                    this.showPop = true
                    this.$nextTick(() => {
                        // 此时才能获取refs引用
                        this.$refs.pop.doShow()
                    })
                })
            }
        }
    };
    var Ctor = Vue.extend(Main)
    new Ctor().$mount('#app')
</script>
</body>
</html>

性能分析

  1. 首先el-popover不参与循环肯定会大大缩短首次渲染时间,但是如果循环次数少或者el-popover嵌套的内容不复杂,其实也没必要提出来
  2. 缺点就是 点击的时候再渲染popover等待时间会相对比for循环渲染好要长。
  3. 如果想减少进页面的首次渲染时间可以提出popover,如果想减少点击显示popover的时间就在for循环中提前渲染好
首屏渲染时间对比

在本地打开html,分别在beforeCreate 和 mounted中打印,比较渲染时间,具体时间要看各自电脑性能,下面写的是在i7的情况下

        beforeCreate(){
            console.time('per')
        },
        mounted(){
            let time = console.timeEnd('per')
        },
  1. 上述代码将popover参与循环的情况下为 per: 80.890869140625 ms
  2. 不参与循环的情况下为 per: 30.35498046875 ms

对比下时间快了两倍多,如果popover嵌套el-table等复杂内容时首屏渲染时间提升效果会更明显

在线运行 或者直接复制上面的html本地打开

<template> <div class="label-container"> <div style="border: 1px solid #d9d9d9"></div> <div class="header"> <div class="biaoqian">客户特征标签</div> <el-popover placement="right" popper-class="group-popover" width="287" trigger="click"> <div class="full-label-section"> <span>业务需求</span> <div class="full-tag-group"> <el-popover v-for="(item, index) in requireList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in requireList" v-if="!shouldShowPopover(item)" :key="index" size="mini" >{{ item.labelName }}</el-tag> </div> </div> <div class="full-label-section"> <span>业务特征</span> <div class="full-tag-group"> <el-popover v-for="(item, index) in featureList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" type="" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in featureList" v-if="!shouldShowPopover(item)" :key="index" type="success" size="mini" >{{ item.labelName }}</el-tag> </div> </div> <div class="full-label-section full-label-sections"> <span>拓展任务</span> <div class="full-tag-group"> <el-popover v-for="(item, index) in taskList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" type="danger" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in taskList" v-if="!shouldShowPopover(item)" :key="index" type="danger" size="mini" >{{ item.labelName }}</el-tag> </div> </div> <el-button slot="reference" class="btn">更多</el-button> </el-popover> </div> <!-- 客户特征部分标签 --> <div style="padding: 10px; background: #fafafa"> <div class="label-section"> <span>业务需求</span> <div class="tag-group"> <el-popover v-for="(item, index) in showRequireList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in showRequireList" v-if="!shouldShowPopover(item)" :key="index" size="mini" >{{ item.labelName }}</el-tag> </div> </div> <div class="label-section"> <span>业务特征</span> <div class="tag-group"> <el-popover v-for="(item, index) in showFeatureList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" type="success" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in showFeatureList" v-if="!shouldShowPopover(item)" :key="index" type="success" size="mini" >{{ item.labelName }}</el-tag> </div> </div> <div class="label-section"> <span>拓展任务</span> <div class="tag-group"> <el-popover v-for="(item, index) in showTaskList" :key="index" placement="top-start" popper-class="grid-popover" trigger="hover" :content="item.labelName" v-if="shouldShowPopover(item)" > <el-tag slot="reference" type="danger" size="mini" class="truncate-tag" >{{ item.labelName }}</el-tag> </el-popover> <el-tag v-for="(item, index) in showTaskList" v-if="!shouldShowPopover(item)" :key="index" type="danger" size="mini" >{{ item.labelName }}</el-tag> </div> </div> </div> </div> </template> <script> import { Tag, Button, Popover,Tooltip } from 'element-ui'; export default { name: 'groupLabelBox', components: { [Tag.name]: Tag, [Button.name]: Button, [Popover.name]: Popover, [Tooltip.name]: Tooltip, }, props: { requireList: { type: Array, required: true, }, featureList: { type: Array, required: true, }, taskList: { type: Array, required: true, }, }, data() { return { dialogVisible: false, }; }, computed: { showRequireList() { return this.requireList.slice(0, 2); }, showFeatureList() { return this.featureList.slice(0, 2); }, showTaskList() { return this.taskList.slice(0, 2); }, }, methods: { shouldShowPopover(item) { return item?.labelName?.length >= 7 }, }, }; </script> <style lang="less" scoped> .group-popover { padding: 30px !important; /* 标题样式 */ .full-label-section{ padding-bottom: 12px; > span { display: block; margin-bottom: 10px; width: 56px; height: 14px; font-family: PingFangSC, PingFang SC, sans-serif; font-weight: 400; font-size: 14px; color: #888888; line-height: 14px; text-align: left; font-style: normal; } } /* 标签组样式 */ .full-tag-group { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 6px; .el-tag { margin: 0 !important; border-radius: 4px; font-size: 12px; padding: 0 8px; height: 24px; line-height: 24px; } } .full-label-sections { padding-bottom: 0 !important; } } .truncate-tag { display: inline-block; max-width: 112px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; cursor: default; } .truncate-tag:hover:after { position: absolute; top: 30px; left: 10px; content: attr(data-title); color: #333333; } .grid-popover { min-width: 54px; text-align: center; padding: 10px 10px; } .label-container { position: relative; padding: 0px 14px 7px 16px; } .header { display: flex; justify-content: space-between; align-items: center; margin: 10px 0px; .biaoqian { width: 84px; height: 14px; font-family: PingFangSC, PingFang SC, sans-serif; font-weight: 400; font-size: 14px; color: #888888; line-height: 14px; text-align: left; font-style: normal; } .btn { width: 33px; height: 17px; padding: 0 4px; background: #ffffff; border-radius: 2px; border: 1px solid #cbcbcb; font-weight: 400; font-size: 12px; color: #333333; line-height: 17px; font-style: normal; > span { display: block; width: fit-content; } } } .label-section { > span { display: block; margin-bottom: 5px; width: 48px; height: 14px; font-family: PingFangSC, PingFang SC, sans-serif; font-weight: 400; font-size: 12px; color: #333333; line-height: 14px; text-align: left; font-style: normal; } } .tag-group { display: flex; gap: 10px; flex-wrap: wrap; } .label-section:not(:last-child) { margin-bottom: 10px; } </style> 优化简化以上代码
最新发布
07-13
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值