飞雪连天射白鹿,笑书神侠倚碧鸳

0%

接口返回10万条数据前端如何加载

震惊!后端大哥偷懒不分页,前端如何在不影响性能时加载大量数据

原文:刘小夕


1
2
3
4
5
<div>
<ul id="appScrollData"></ul>
</div>
<div id="app"></div>
<div id="poll"></div>

初级方案,直接渲染

数据量庞大将导致页面性能极具降低, 造成页面卡顿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 此处直接使用了原文中的获取数据方式,只做了同步转换
function getData() {
return new Promise((res, rej) => {})
}
// 普通创建对象
function way1() {
getData().then(res => {
console.log('开始创建')
console.time('普通创建对象')
let box = document.createElement("ul");
for (let i = 0; i < res.data.length; i++) {
let div1 = document.createElement("li");
div1.innerHTML = `${res.data[i].title} ${res.data[i].name} ${res.data[i].text}`
box.appendChild(div1)
}
console.log(box, res.data)
app.appendChild(box)
setTimeout(() => {
console.timeEnd('普通创建对象')
})
})
}
// 文档碎片创建对象
function way2() {
getData().then(res => {
console.log('开始创建')
console.time('文档碎片创建对象')
var box = document.createDocumentFragment();
for (let i = 0; i < res.data.length; i++) {
let div1 = document.createElement("li");
div1.innerHTML = `${res.data[i].title} ${res.data[i].name} ${res.data[i].text}`
box.appendChild(div1)
}
console.log(box, res.data)
app.appendChild(box)
setTimeout(() => {
console.timeEnd('文档碎片创建对象')
})
})
}
// way1();
// way2();

这里用到了 console.time/console.timeEnd 计算函数运行时长
注意为什么console.timeEnd放在了setTimeout中?
事件循环先宏任务,再微任务,最后更新ui
setTimeout属于宏任务
这里需要得到能够渲染数据后的运行时长,所以放在了下一轮宏任务中,能够准确获取总

1
2
3
4
// 10w数据  普通                    文档碎片
// chrome 10521.47412109375ms 12217.3671875ms
// edge 8409.425048828125ms 8900.69287109375ms
// firefox 828ms 卡崩溃了

(createDocumentFragment真的更快吗?大家可以测试告诉我)

中级方案,懒加载+分页

渲染首屏,后续窗口滚动时防抖优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
let getApiData = [];
let way3Data;

// 分页
let data = [];
let curPage = 1; // 当前的页数
let pageSize = 30; // 每一页展示的数量 * data 传入的数据量
let prevY = 0;

function way3() {
getData().then(res => {
getApiData = res.data;
})
}
function debounce(fn, delay) {
let time = '';
return function(...args) {
clearTimeout(time);
time = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}

// 监听scroll事件,使用getBoundingClientRect获取底部空占位元素相对于可视窗口的距离进行懒加载
// 回滚时需要单向锁
function scrollAndLoading() {
if (window.scrollY > prevY) { // 判断用户是否向下滚动
prevY = window.scrollY; // 在向下滚动并且滚动高度大于上一次时更新
if (poll.getBoundingClientRect().top <= window.innerHeight) {
if (getApiData.length == 0) return console.log('getApiData数据还未请求返回');
// 请求下一页数据,每次渲染量为pageSize
curPage++;
way3Data = getApiData.slice(pageSize * (curPage - 1), pageSize * curPage)
console.log('开始创建', way3Data.length / getApiData.length)
console.time('文档碎片创建对象')
var box = document.createDocumentFragment();
for (let i = 0; i < way3Data.length; i++) {
let div1 = document.createElement("li");
div1.innerHTML = `${way3Data[i].title} ${way3Data[i].name} ${way3Data[i].text}`
box.appendChild(div1)
}
console.log(box, way3Data)
app.appendChild(box)
setTimeout(() => {
console.timeEnd('文档碎片创建对象')
})
}
}
}
// way3();
// const getWay3Data = debounce(scrollAndLoading, 300)
// window.addEventListener('scroll', getWay3Data, false)

高级方案,更优雅的实现方式

  • js缓冲器来分片处理
  • 虚拟长列表 核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容
  • web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算
  • 模糊搜索,二分法优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 这里的写法每轮是取数组第一项,并执行添加
function multistep(steps, taskHandle, callback) {
// var tasks = steps.concat();
var tasks = steps;
setTimeout(function() {
var task = tasks.shift(); // 每次执行一步
// task.apply(null, args || []); //调用Apply参数必须是数组
taskHandle(task);

if (tasks.length > 0) {
// arguments.callee代表multistep函数
setTimeout(arguments.callee, 25);
} else {
callback();
}
}, 25);
}

function way4() {
getData().then(res => {
getApiData = res.data;
multistep(getApiData, function(_arr) {
// console.time('文档碎片创建对象')
let box = document.createDocumentFragment();
let div1 = document.createElement("li");
div1.innerHTML = `${_arr.title} ${_arr.name} ${_arr.text}`;
box.appendChild(div1);
app.appendChild(box);
// setTimeout(() => {
// console.timeEnd('文档碎片创建对象')
// })
}, function() {
console.log('way4完成')
})
})
}
// way4()

这里用分片处理,每个事件循环周期处理几十条数据,每轮5ms
步骤的集合,每次执行的依赖参数,完成结果回调


无论采取什么方案,最终目的是释放主线程,让用户可进行交互操作时,无卡顿感知

听说,打赏我的人最后都找到了真爱
↘ 此处应有打赏 ↙
// 用户脚本