本文参考主要参考
- (Transfer)解决:Element-ui 中 Transfer 穿梭框因数据量过大而渲染卡顿问题的三种方法
- Element-UI的transfer穿梭框组件数据量大解决方案
- (对element-plus的组件二次修改-自定义组件 - 掘金 (juejin.cn))
前言:
产品需求中需要用到穿梭器,并且需要一次性将数据添加至穿梭器,在接口返回几百条时就已经明显慢了,返回上千条时页面加载都要等好几秒了。
经过网上文章检索,花时间把element-plus中el-transfer源码大致看了一下,本来是准备参考文章1进行虚拟列表优化,但是在查看虚拟化树形控件(tree-v2)时发现自带的虚拟化列表virtual-list中的FixedSizeList
,并且对部分源码transfer用通义灵码(个人感觉非常好用)进行优化。
附源码:点击前往
文章使用示例:点击前往
一、话不多说,看实际应用效果
1.官方渲染1W条数据使用时间28秒左右,期间整体页面卡顿不可操作,毕竟1W个dom渲染还是挺耗时的,等到浏览器反应过来了才可以滚动查看数据
2.优化后虚拟列表渲染时间,120毫秒
3.这里用到的页面加载计时函数
// 在页面加载完成后调用该函数,可以获取到页面渲染时间
function getPageLoadTime() {
var startTime = performance.now();
window.addEventListener('load', function() {
var endTime = performance.now();
var loadTime = endTime - startTime;
console.log('页面渲染时间为:' + loadTime + '毫秒');
});
}
4.实际效果对比-使用官方组件:
5.实际效果对比-使用虚拟化组件:
小结:
整体来说比官方组件流畅,但是需要注意默认组件增加itemSize、height,用于虚拟列表计算高度,itemSize需要根据实际稍微增加部分来,这里使用的与列表相同的高度itemSize=30,最后几条数据没有渲染出来
将itemSize设置为40后正常
这里最后一条的样式是el-checkbox的默认样式,需要手动deep添加覆盖
:deep(.el-checkbox:last-of-type){
margin-right: 30px;
}
二、部分代码优化(使用通义灵码)
先贴一张transfer源码目录结构图,通过代码优化多数都是优化includes的校验,使用Set()
1. 优化use-check
1.1 全选updateAllChecked()
const updateAllChecked = () => {
// 检查 checkableData.value 是否为空
if (checkableData.value.length === 0) {
panelState.allChecked = false;
return;
}
// 提取数据键值,假设 propsAlias.value.key 是 'id' 或类似的唯一标识符
const dataKeys = checkableData.value.map(
(dataItem) => dataItem[propsAlias.value.key]
);
// 将 panelState.checked 转换为 Set 以提高查找性能
const checkedSet = new Set(panelState.checked);
// 更新 allChecked 的状态
panelState.allChecked = dataKeys.length > 0 && dataKeys.every((dataKey) => checkedSet.has(dataKey));
}
1.2 优化选中状态panelState.checked的watch
watch(
() => panelState.checked,
(val, oldVal) => {
updateAllChecked()
if (panelState.checkChangeByUser) {
// 使用 Set 来存储值,以提高查找效率
const setVal = new Set(val);
const setOldVal = new Set(oldVal);
// 计算新增加和被移除的键
const addedKeys = Array.from(setVal).filter(v => !setOldVal.has(v));
const removedKeys = Array.from(setOldVal).filter(v => !setVal.has(v));
const movedKeys = [...addedKeys, ...removedKeys];
emit(CHECKED_CHANGE_EVENT, val, movedKeys);
} else {
emit(CHECKED_CHANGE_EVENT, val);
// 在这里设置为true,表示下一次变化是由用户引起的
panelState.checkChangeByUser = true;
}
}
)
1.3 优化props.data的watch
watch(
() => props.data,
() => {
// 假设TransferKey为string类型
const checked: TransferKey[] = []
const filteredDataKeys = new Set(filteredData.value.map(
(item) => item[propsAlias.value.key] as TransferKey // 确保类型正确
))
panelState.checked.forEach((item) => {
if (filteredDataKeys.has(item)) {
checked.push(item)
}
})
// 修改状态前的注释
// 清除用户触发的检查变更标志,并更新已选中的项
panelState.checkChangeByUser = false
panelState.checked = checked
}
)
1.4 优化默认选中props.defaultChecked的watch
watch(
() => props.defaultChecked,
(val, oldVal) => {
// 保留原有的逻辑,即使 val 和 oldVal 完全相同也会更新 checked
const checkableDataKeysSet = new Set(checkableData.value.map(
(item) => item[propsAlias.value.key]
));
// 使用 Set 的 has 方法来填充 checked 数组
const checked: TransferKey[] = val.filter(item => checkableDataKeysSet.has(item));
panelState.checkChangeByUser = false;
panelState.checked = checked;
},
{
immediate: true,
}
)
2.优化use-computed-data
2.1 优化sourceData() targetData() 主要是使用Set替换includes
// 创建一个Set用于快速查找
const modelValueSet = computed(() => new Set(props.modelValue));
// 过滤函数
const sourceData = computed(() => {
//原代码
// props.data.filter((item) => !props.modelValue.includes(item[propsAlias.value.key]))
if (!Array.isArray(props.data)) return [];
return props.data.filter((item) => {
if (typeof item !== 'object' || item === null) return false;
const key = propsAlias.value.key;
if (key === undefined || typeof key !== 'string') return false;
const value = item[key];
return !modelValueSet.value.has(value);
});
});
// 过滤函数
const targetData = computed(() => {
if (props.targetOrder === 'original') {
// 原代码
// return props.data.filter((item) => props.modelValue.includes(item[propsAlias.value.key]))
return props.data.filter((item) => {
const key = propsAlias.value.key;
const value = item[key];
return modelValueSet.value.has(value);
});
} else {
return props.modelValue.reduce(
(arr, cur) => {
const val = props.dataObj.value[cur];
if (val) {
arr.push(val);
}
return arr;
},
[]
);
}
});
3.优化use-move的addToLeft(),addToRight()
3.1 addToLeft()
const addToLeft = () => {
// 获取当前值的副本以避免修改原始数据
const currentValue = [...props.modelValue];
// 创建一个临时数组用于存储要移除的元素
const itemsToRemove:any = [];
// 遍历 rightChecked 数组
checkedState.rightChecked.forEach((item) => {
const index = currentValue.indexOf(item);
if (index > -1) {
// 记录需要移除的元素
itemsToRemove.push(item);
}
});
// 使用 filter 方法来移除元素,这样更高效
const filteredArray = currentValue.filter(item => !itemsToRemove.includes(item));
// 更新值
_emit(filteredArray, 'left', checkedState.rightChecked);
}
3.2 addToRight()
const addToRight = () => {
let currentValue = props.modelValue.slice()
// 将数组转换为集合以加快查找速度
const leftCheckedSet = new Set(checkedState.leftChecked);
const modelValueSet = new Set(props.modelValue);
// 筛选和映射需要移动的items
const itemsToBeMoved = props.data
.filter((item: TransferDataItem) => {
// 确保密钥存在于项目中
const itemKey = item[propsAlias.value.key];
// 检查item是否已被选中,并且尚未在目标列表中
return leftCheckedSet.has(itemKey) && !modelValueSet.has(itemKey);
})
.map((item: TransferDataItem) => item[propsAlias.value.key]);
currentValue =
props.targetOrder === 'unshift'
? itemsToBeMoved.concat(currentValue)
: currentValue.concat(itemsToBeMoved)
if (props.targetOrder === 'original') {
const currentValueSet = new Set(currentValue);
currentValue = props.data
.filter((item) => currentValueSet.has(item[propsAlias.value.key]))
.map((item) => item[propsAlias.value.key]);
}
_emit(currentValue, 'right', checkedState.leftChecked)
}
二、组件优化部分
1.在transfer.ts中的transferProps增加两个入参itemSize、height
itemSize: {
type: Number,
default: 26,
},
height: {
type: Number,
default: 200,
},
2.transfer-panel.ts中transferPanelProps接收参数取transferProps的itemSize、height
3.transfer-panel.vue中使用fixed-size-list包裹el-checkbox
这里需要注意对fixed-size-list定义虚拟化渲染的容器为containerElement="label",因为checkbox最后渲染包裹的是“label”标签,如果不加将列表将会滚动,下面的不会渲染
<el-checkbox-group
v-show="!hasNoMatch && !isEmpty(data)"
v-model="checked"
:validate-event="false"
:class="[ns.is('filterable', filterable), ns.be('panel', 'list')]"
style="overflow: hidden;"
>
<!-- 虚拟化列表 -->
<fixed-size-list
:class-name="ns.b('virtual-list')"
:data="filteredData"
containerElement="label"
:total="filteredData.length"
:height="height"
:item-size="itemSize"
>
<template #default="{ data , index }">
<el-checkbox
:key="data[index][propsAlias.key]"
:class="ns.be('panel', 'item')"
:label="data[index][propsAlias.key]"
:disabled="data[index][propsAlias.disabled]"
:validate-event="false"
>
<option-content :option="optionRender?.(data[index])" />
</el-checkbox>
</template>
</fixed-size-list>
</el-checkbox-group>
4.调整多个文件的引入方式
涉及多个文件改动,这里简单介绍统一的改动调整引入
//源码
import { useLocale, useNamespace } from '@element-plus/hooks'
import { ElCheckbox, ElCheckboxGroup } from '@element-plus/components/checkbox'
import { ElInput } from '@element-plus/components/input'
import { FixedSizeList } from '@element-plus/components/virtual-list'
//改动:饿了吗组件相关、hook相关的引入都可以直接调整成一行按需引入
import { useLocale, useNamespace ,FixedSizeList ,ElCheckbox ,ElCheckboxGroup ,ElInput } from 'element-plus'
//使用@element-plus/utils统一调整成'element-plus/es/utils/index'
import { isEmpty } from '@element-plus/utils'
import { isEmpty } from 'element-plus/es/utils/index'
写在最后:第一次掘金发文章,写得不好,请见谅