滑动条防抖
防抖:滑动条频繁操作时只取最后一次操作的结果
<template>
<el-slider size="small"
:max="30"
v-model="tempSecFilter"
@input="changeSecFilter"
/>
</template>
<script>
import _, { debounce } from "lodash";
// 滑动条响应式和防抖
let secFilter = ref(30)
let tempSecFilter = ref(30)
let changeSecFilter = debounce((value) => {
secFilter.value = value;
tempSecFilter.value = secFilter.value
// secFilter更新会自动触发 DEScatter 更新
}, 300) //防抖时间
//手写防抖
function debounce(fn, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, args)
}, delay);
};
}
</script>
时间切片优化渲染
原理: 把性能消耗大的操作切片,先执行第一片,让页面先渲染第一片,渲染完成后再操作下一片(在用户浏览的间隙在后台悄悄做)。绝对速度并未改变,但通过安排操作顺序,使首屏速度up,用户体验up。
怎么做? 把每个切片操作放到Requestanimation 回调,他执行完一个后会等浏览器渲染完成再执行下一个,自动完成分片执行操作。
前端: 把获取表格数据(文件)的操作分片,对每一片:
——> 先请求首屏要用的 50 条数据(url 里拼接起始行和结束行的参数)
——> 解析数据:如果是起始行号不是 0,则需要手动拼接一个字段行
——> 筛选数据、渲染(追加入响应式数据)
——> 在用户浏览的时候,在后台请求51-100的数据
——> ...
// 优化:用时间分片请求数据:
// 先请求首屏要用的 50 条数据 ——> 筛选、渲染 ——> 请求51-100的数据 ——> ...
const loadDataByLine = async () => {
let chunkSize = 50 //每片加载多少条数据
let start = 0
let totalLines = 0 //总行数
let header = null //csv 的字段名
function sliceRender() {
requestAnimationFrame(async () => {
const res = await axios.get(
`http://localhost:3000/data/getTblByLine?start=${start}&end=${start+chunkSize}`
);
// 解析返回数据
totalLines = res.data.totalLines
let temp = []
if (!header && start === 0) {
// 如果是第一次加载,提取字段行
header = res.data.lines[0];
}
if (start > 0 && header) {
// 如果不是第一次加载,手动添加字段行
temp.push(header);
}
temp.push(...res.data.lines);
const csvString = temp.join('\n');
dataAll = d3.csvParse(csvString, d3.autoType)
// 初次筛选数据 ---> 初次渲染数据
await processData();
// console.log("本次请求:" + start + "-" + Number(start + chunkSize))
// 如果还有未处理的数据,继续调度下一个切片
start = start + chunkSize
if (start < totalLines) {
sliceRender();
}
});
}
sliceRender();
}
3、后端
- 使用
readline模块逐行读取 csv 文件。 - 根据请求参数,筛选特定范围的行,并以数组形式返回 lines
exports.getTblByLine = async function (req,res){
const filePath = path.join( dataPath,'faced_result_clisa_cls2_12-24.csv');
const startLine = parseInt(req.query.start) || 0;
const endLine = parseInt(req.query.end) || startLine + 50;
const readStream = fs.createReadStream(filePath);
const rl = readline.createInterface({ input: readStream });
let currentLine = 0;
let lines = [];
let totalLines = 0;
// 筛选前端需要的行
for await (const line of rl) {
if (currentLine >= startLine && currentLine <= endLine) {
lines.push(line);
}
// if (currentLine > endLine) break;
currentLine++;
}
totalLines = currentLine; // 计算得到的总行数
res.json({
totalLines: totalLines,
lines: lines
});
// res.json(lines);
}
表格懒加载
只显示可见区域的表格项:监控滚动事件 ---> 滚动时 ScrollTop 更新 ---> start 和 end 一起更新 ---> 截取 start-end 条显示 ---> 可见列表项始终只有 start-end 条
使用了自定义指令和节流技术实现了 el-table 的懒加载,具体思路如下:
-
注册自定义指令:绑定到 el-table 上,通过指令监听滚动事件。
-
获取滚动容器和表格主体:自定义指令中,获取: el-table 的
- 滚动容器高度(
.el-table__body-wrapper) - 表格主体高度(可视)
- 表格项高度
- 滚动容器高度(
-
计算渲染范围:根据滚动容器的
scrollTop和 固定表格项高度itemHeight值,实时计算需要渲染的表格项的起点和终点。(computed里 ↓)start = scrollTop / itemHeightend = ( scrollTop + tableVisHeight ) / itemHeight
-
渲染数据:切割并渲染 start-end 的数据项
-
动态填充距离:根据计算出的起点和终点,动态设置表格主体的上下填充距离(padding 用来撑起容器的高度),保持表格容器总高度的一致性。
paddingTop = start * itemHeightpaddingBottom =(dataLen - end)* itemHeight
-
节流滚动事件处理:使用
lodash库的throttle限制滚动事件的触发频率,确保在特定时间段内只执行一次滚动处理函数。
<template>
<el-table
v-scroll
:data="tableData.slice(start,end)" <!--//4.切割割+渲染数据-->
height = "266"
>
....
<el-table>
</el-table>
<script setup>
//...
// 定义滚动相关的响应式数据
let tableData = ref([...]) //表格数据
const scrollTop = ref(0);
const tableVisHeight = ref(226); //表格的可视区高度
const itemHeight = ref(42); // 每个表格项高度
// 3. 计算需要渲染范围
const start = computed(() => {
return Math.max(scrollTop.value / itemHeight.value - 5, 0);
});
const end = computed(() => {
return Math.min((scrollTop.value + tableVisHeight.value) / itemHeight.value + 5, tableData.value.length);
});
// 5. 计算滚动条的上下填充距离
const padding = computed(() => {
let paddingBottom = (tableData.value.length - end.value) * itemHeight.value;
let paddingTop = start.value * itemHeight.value;
return [paddingTop, paddingBottom];
});
// 1. 自定义指令
const tableRef = ref(null)
const vScroll = {
mounted: (el) => {
// const tableWrapper = el.querySelector(".el-table__body-wrapper");//在此处无效,原因不明
const tableWrapper = tableRef.value.$refs.scrollBarRef.wrapRef;
// 6. 优化后的节流操作
if (tableWrapper) {
tableWrapper.addEventListener("scroll", throttle(() => handleScroll(tableWrapper), 200));
} else {
console.error("Table wrapper element not found");
}
}
};
// 滚动事件处理函数
let handleScroll = (tableWrapper) => {
const tableBody = tableWrapper.querySelector(".el-table__body");
const tableItem = tableWrapper.querySelector(".el-table__row");
if (tableBody && tableItem) {
// 2. 获取滚动容器和表格主体(一次即可)
itemHeight.value = tableItem.clientHeight;
tableVisHeight.value = tableWrapper.clientHeight;
// 3. 实时更新ScrollTop ---> start和end更新
scrollTop.value = tableWrapper.scrollTop;
// 4. 更新上下填充距离
tableBody.style.paddingTop = padding.value[0] + "px";
tableBody.style.paddingBottom = padding.value[1] + "px";
}
}
</script>