前端工程师如何处理10万条数据

前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死. 后面他还说需要支持搜索, 也是前端来实现,我顿时产生了兴趣. 当时想到的方案大致如下:

采用懒加载+分页(前端维护懒加载的数据分发和分页)

使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)

懒加载和分页方式一般用于做长列表优化, 类似于表格的分页功能, 具体思路就是用户每次只加载能看见的数据, 当滚动到底部时再去加载下一页的数据.

虚拟滚动技术也可以用来优化长列表, 其核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容,实现思路也非常简单.

通过以上分析其实已经可以解决朋友的问题了,但是最为一名有追求的前端工程师, 笔者认真梳理了一下,并基于第一种方案抽象出一个实际的问题:

如何渲染大数据列表并支持搜索功能?

笔者将通过模拟不同段位前端工程师的实现方案, 来探索一下该问题的价值. 希望能对大家有所启发, 学会真正的深入思考.

正文

笔者将通过不同经验程序员的技术视角来分析以上问题, 接下来开始我们的表演.

在开始代码之前我们先做好基础准备, 笔者先用nodejs搭建一个数据服务器, 提供基本的数据请求,核心代码如下:

app.use(async(ctx, next) => {

if(ctx.url ==='/api/getMock') {

letlist = []

// 生成指定个数的随机字符串

functiongenrateRandomWords(n) {

letwords ='abcdefghijklmnopqrstuvwxyz你是好的嗯气短前端后端设计产品网但考虑到付款啦分手快乐的分类开发商的李开复封疆大吏师德师风吉林省附近',

len = words.length,

ret =''

for(leti=0; i< n; i++) {

ret += words[Math.floor(Math.random() * len)]

}

returnret

}

// 生成10万条数据的list

for(leti =0; i<100000; i++) {

list.push({

name:`xu_0${i}`,

title: genrateRandomWords(12),

text:`我是第${i}项目, 赶快🌀吧~~`,

tid:`xx_${i}`

})

}

ctx.body = {

state:200,

data: list

}

}

awaitnext()

})

复制代码

以上笔者是采用koa实现的基本的mock数据服务器, 这样我们就可以模拟真实的后端环境来开始我们的前端开发啦(当然也可以直接在前端手动生成10万条数据). 其中genrateRandomWords方法用来生成指定个数的字符串,这在mock数据技术中应用很多, 感兴趣的盆友可以学习了解一下. 接下来的前端代码笔者统一采用react来实现(vue同理).

初级工程师的方案

直接从后端请求数据, 渲染到页面的硬编码方案,思路如下:

代码可能是这样的:

请求后端数据:

fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {

if(res.state) {

data = res.data

setList(data)

}

})

复制代码

渲染页面

{

list.map((item, i) => {

return

{item.title}{item.name}</span></div>

<div>{item.text}</div>

</div>

})

}

复制代码

搜索数据

consthandleSearch = (v) => {

letsearchData = data.filter((item, i) => {

returnitem.title.indexOf(v) >-1

})

setList(searchData)

}

复制代码

这样做本质上是可以实现基本的需求,但是有明显的缺点,那就是数据一次性渲染到页面中, 数据量庞大将导致页面性能极具降低, 造成页面卡顿.

中级工程师的方案

作为一名有一定经验的前端开发工程师,一定对页面性能有所了解, 所以一定会熟悉防抖函数和节流函数, 并使用过诸如懒加载和分页这样的方案, 接下来我们看看中级工程师的方案:

通过这个过程的优化, 代码已经基本可用了, 下面来介绍具体实现方案:

懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下:

这里我们通过监听window的scroll事件以及对poll元素使用getBoundingClientRect来获取poll元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.

在滚动的过程汇总我们还需要注意一个问题就是当用户往回滚动时, 实际上是不需要做任何处理的,所以我们需要加一个单向锁, 具体代码如下:

functionscrollAndLoading() {

if(window.scrollY > prevY) {// 判断用户是否向下滚动

prevY =window.scrollY

if(poll.current.getBoundingClientRect().top <=window.innerHeight) {

// 请求下一页数据

}

}

}

useEffect(() => {

// something code

constgetData = debounce(scrollAndLoading,300)

window.addEventListener('scroll', getData,false)

return() => {

window.removeEventListener('scroll', getData,false)

}

}, [])

复制代码

其中prevY存储的是窗口上一次滚动的距离, 只有在向下滚动并且滚动高度大于上一次时才更新其值.

至于分页的逻辑,原生javascript实现分页也很简单, 我们通过定义几个维度:

curPage当前的页数

pageSize 每一页展示的数量

data 传入的数据量

有了这几个条件,我们的基本能分页功能就可以完成了. 前端分页的核心代码如下:

letdata = [];

letcurPage =1;

letpageSize =16;

letprevY =0;

// other code...

functionscrollAndLoading() {

if(window.scrollY > prevY) {// 判断用户是否向下滚动

prevY =window.scrollY

if(poll.current.getBoundingClientRect().top <=window.innerHeight) {

curPage++

setList(searchData.slice(0, pageSize * curPage))

}

}

}

复制代码

防抖函数实现防抖函数因为比较简单, 这里直接上一个简单的防抖函数代码:

functiondebounce(fn, time) {

returnfunction(args) {

letthat =this

clearTimeout(fn.tid)

fn.tid = setTimeout(() => {

fn.call(that, args)

}, time);

}

}

复制代码

搜索实现 搜索功能代码如下:

consthandleSearch = (v) => {

curPage =1;

prevY =0;

searchData = data.filter((item, i) => {

// 采用正则来做匹配, 后期支持前端模糊搜索

letreg =newRegExp(v,'gi')

returnreg.test(item.title)

})

setList(searchData.slice(0, pageSize * curPage))

}

复制代码

需要结合分页来实现, 所以这里为了不影响源数据, 我们采用临时数据searchData来存储. 效果如下:

搜索后:

无论是搜索前还是搜索后, 都利用了懒加载, 所以再也不用担心数据量大带来的性能瓶颈了~

高级工程师的方案

作为一名久经战场的程序员, 我们应该考虑更优雅的实现方式,比如组件化, 算法优化, 多线程这类问题, 就比如我们问题中的大数据渲染, 我们也可以用虚拟长列表来更优雅简洁的来解决我们的需求. 至于虚拟长列表的实现笔者在开头已经点过,这里就不详细介绍了, 对于更大量的数据,比如100万(虽然实际开发中不会遇到这么无脑的场景),我们又该怎么处理呢?

第一个点我们可以使用js缓冲器来分片处理100万条数据, 思路代码如下:

functionmultistep(steps,args,callback){

vartasks = steps.concat();

setTimeout(function(){

vartask = tasks.shift();

task.apply(null, args || []);//调用Apply参数必须是数组

if(tasks.length >0){

setTimeout(arguments.callee,25);

}else{

callback();

}

},25);

}

复制代码

这样就能比较大量计算导致的js进程阻塞问题了.更多性能优化方案可以参考笔者之前的文章:

web性能优化的15条实用技巧

我们还可以通过web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算, 计算完成后再通过web worker的通信机制来通知主进程, 比如模糊搜索等, 我们还可以对搜索算法进一步优化,比如二分法等,所以这些都是高级工程师该考虑的问题. 但是一定要分清场景, 寻找出性价比更高的方案.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352