当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

徐小夕 等级 345 0 0

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

  1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)
  2. 使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)

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

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

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

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

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

正文

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

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

app.use(async (ctx, next) => {
  if(ctx.url === '/api/getMock') {
    let list = []

    // 生成指定个数的随机字符串
    function genrateRandomWords(n) {
      let words = 'abcdefghijklmnopqrstuvwxyz你是好的嗯气短前端后端设计产品网但考虑到付款啦分手快乐的分类开发商的李开复封疆大吏师德师风吉林省附近',
          len = words.length,
          ret = ''
      for(let i=0; i< n; i++) {
        ret += words[Math.floor(Math.random() * len)]
      }
      return ret
    }

    // 生成10万条数据的list
    for(let i = 0; i< 100000; i++) {
      list.push({
        name: `xu_0${i}`,
        title: genrateRandomWords(12),
        text: `我是第${i}项目, 赶快🌀吧~~`,
        tid: `xx_${i}`
      })
    }

    ctx.body = {
      state: 200,
      data: list
    }
  }
  await next()
})

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

初级工程师的方案

直接从后端请求数据, 渲染到页面的硬编码方案,思路如下: 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理? 代码可能是这样的:

  1. 请求后端数据:
    fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
    if(res.state) {
     data = res.data
     setList(data)
    }
    })
  2. 渲染页面
    {
     list.map((item, i) => {
       return <div className={styles.item} key={item.tid}>
         <div className={styles.tit}>{item.title} <span className={styles.label}>{item.name}</span></div>
         <div>{item.text}</div>
       </div>
     })
    }
  3. 搜索数据
    const handleSearch = (v) => {
     let searchData = data.filter((item, i) => {
       return item.title.indexOf(v) > -1
      })
      setList(searchData)
    }
    这样做本质上是可以实现基本的需求,但是有明显的缺点,那就是数据一次性渲染到页面中, 数据量庞大将导致页面性能极具降低, 造成页面卡顿.

    中级工程师的方案

    作为一名有一定经验的前端开发工程师,一定对页面性能有所了解, 所以一定会熟悉防抖函数节流函数, 并使用过诸如懒加载分页这样的方案, 接下来我们看看中级工程师的方案: 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

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

  1. 懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下: 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理? 这里我们通过监听windowscroll事件以及对poll元素使用getBoundingClientRect来获取poll元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.

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

function scrollAndLoading() {
    if(window.scrollY > prevY) {  // 判断用户是否向下滚动
      prevY = window.scrollY
      if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
        // 请求下一页数据
      }
    }
}

useEffect(() => {
    // something code
    const getData = debounce(scrollAndLoading, 300)
    window.addEventListener('scroll', getData, false)
    return () => {
      window.removeEventListener('scroll', getData, false)
    }
  }, [])

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

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

  • curPage当前的页数
  • pageSize 每一页展示的数量
  • data 传入的数据量

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

let data = [];
let curPage = 1;
let pageSize = 16;
let prevY = 0;

// other code...

function scrollAndLoading() {
    if(window.scrollY > prevY) {  // 判断用户是否向下滚动
      prevY = window.scrollY
      if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
        curPage++
        setList(searchData.slice(0, pageSize * curPage))
      }
    }
}
  1. 防抖函数实现

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

function debounce(fn, time) {
    return function(args) {
      let that = this
      clearTimeout(fn.tid)
      fn.tid = setTimeout(() => {
        fn.call(that, args)
      }, time);
    }
  }
  1. 搜索实现 搜索功能代码如下:
    const handleSearch = (v) => {
      curPage = 1;
      prevY = 0;
      searchData = data.filter((item, i) => {
         // 采用正则来做匹配, 后期支持前端模糊搜索
        let reg = new RegExp(v, 'gi')
        return reg.test(item.title)
      })
      setList(searchData.slice(0, pageSize * curPage))
    }
    需要结合分页来实现, 所以这里为了不影响源数据, 我们采用临时数据searchData来存储. 效果如下: 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理? 搜索后: 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理? 无论是搜索前还是搜索后, 都利用了懒加载, 所以再也不用担心数据量大带来的性能瓶颈了~

高级工程师的方案

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

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

function multistep(steps,args,callback){
    var tasks = steps.concat();

    setTimeout(function(){
        var task = tasks.shift();
        task.apply(null, args || []);   //调用Apply参数必须是数组

        if(tasks.length > 0){
            setTimeout(arguments.callee, 25);
        }else{
            callback();
        }
    },25);
}

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

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

最后

如果想学习更多前端技能,实战学习路线, 欢迎在公众号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。 当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

更多推荐

收藏
评论区

相关推荐

极客手中的利器:Electron
作为一个前端开发人员,你可能已经听说过Electron(https://www.electronjs.org/)了, 你知道VS Code是基于这个技术开发的。 不但VS Code,目前一些大热的软件:飞书、Slack、skype的桌面版都是基于这个技术开发的。 即使如此,这也并不足以引起你的重视, 毕竟桌面软件式微,移动端和Web开发才是大方向
当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?
前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死. 后面他还说需要支持搜索, 也是前端来实现,我顿时产生了兴趣. 当时想到的方案大致如下: 1. 采用
2020年值得你去试试的10个React开发工具
2020年值得你去试试的10个React开发工具 2020年值得你去试试的10个React开发工具 本文由葡萄城技术团队翻译并首发 转载请注明出处:葡萄城官网(https://www.
JavaScript设计模式之英雄联盟
作者:黄梵高 原文: https://juejin.cn/post/6844904165982879758 构造函数模式 简介 在Jav
5. Python 循环的本质就是一段代码懒得重复写
为啥要滚雪球学 Python,目的就是当你学会编程一些思想之后,可以让知识的雪球自行滚动起来。 五、Python 循环的本质就是一段代码懒得重复写 程序中的循环概念非常容易理解,一段相似的代码不想重复去写,然后让程序去完成这个操作就是循环。例如从 1 加到 100,如果你依次去加会发现,代码又臭又长,最好的写法当然是让程序通过循环依次去累加。
AT大牛带你深度剖析Android 10大开源框架
做了几年的Android开发,也面试过不少公司,被面试过,也面试过不少人,其实Android的技术真的是无边界,不管你做过多牛的项目,不管你多久的经验在Android开发中不可能是停滞不前的,需要不断的学习及总结,否则难以解公关技术问题,下面把我压箱底的视频资料贡献给大家; 一,android视频教程 Android视频教程: 1,Android入门
当裸辞遇到了面试难,你需要了解一下这些面试题
乘兴裸辞心甚爽,面试工作屡遭难。 幸得每日一题伴,点击关注莫偷懒。又要到金九银十的跳槽季了,为了让更多的小伙伴可以在面试的时候取的更好的offer,所以自上月起我每天都会在自己的公众号【前端有的玩】里面推送一到两道面试题,俗称每日一题(每日一坑)。方便找工作的小伙伴每日都会有新的收获。本文就是小编将前期的一些比较经典的每日一题进行了梳理,欢迎大家一起
10 大热门人工智能开源工具(框架)
下面是我们今天要讲的10个热门的人工智能开源工具/框架。 1.TensorFlow 10大热门人工智能开源工具(框架)(https://imghelloworld.osscnbeijing.aliyuncs.com/d0aa9
2021涅普冬令营笔记——web
写在前面 老 web 🐕了 课程大纲 2021.2.6——黑客竟在我身边 Burpsuite 功能介绍 安装+ 百度、csdn 都有教程,跳过 proxy模块+ 代理模块,相当于在客户端和服务端中间的一个关卡,可以拦截、修改、丢弃所有的流量包 intruder模块+ 用来爆破的模块 Target Positions Payloads+ Payload Set
程序员怎样写出搞垮公司的代码?
1、乱写注释 注释就像内裤,外面看不见,但是很重要。 注释要严谨,不能有明显的漏洞。如果你的内裤有漏洞,你不尴尬吗?当然了,如果你实力够强大,别人会尴尬。 2、代码和显示不一致 界面上是Post code,代码里是Zip code。看代码看到怀疑人生! 所以说年轻人,你只看到了第二层,你以为我在第一层,实际上我在第五层,你明白我在讲什么吗?
一种极致性能的缓冲队列
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。 背景在多线程下的生产者消费者模型中,需求满足如下情况:对生产者生产投递数据的性能要求非常高多个生产者,单个(多个也可以,本文只介绍单个的情况)消费者当消费者跟不上生产者速度时,可容忍少部分数据丢失生产者是单条单条地生产数据举个日志采集的例子,日志在不同的
小白学习python该往哪方面走小白必看
整理了Python的7大就业方向,希望大家能找到适合自己的,然后学习下去,完成人生的目标。1、Web开发(Python后端)Python有很多优秀的Web开发框架,如Flask、Django、Bootstar等,可以帮助你快速搭建一个网站。当需要一个新功能时,用Python只需添加几行代码即可,这受到了很多初创型公司的一致欢迎。像知乎、豆瓣、小米这样的大厂,
浅析常用的Python Web的几大框架
在各种语言平台中,python涌现的web框架恐怕是最多的,是一个百花齐放的世界,各种microframework、framework不可胜数;猜想原因应该是在python中构造框架十分简单,使得轮子不断被发明。所 以在Python社区总有关于Python框架孰优孰劣的话题。下面就给大家介绍一下python的几大框架: Django Django 应该是最出
测试String, StringBuilder,StringBuffer 的数据,我居然发现这些了~~
懒的搞什么开头了,直接撸代码吧想了想还是给大家看看学姐照片吧,保持眼睛的明亮,代码敲多了,伤眼[](https://shimo.im/docs/9GTP6XrJg9J88cJD/) 1: 作StringBuilder与String的拼接比较c@Testpublic void testString () String s""; long beg
盘点Python网页开发轻量级框架Flask知识(上篇)
前言今天来给大家说一个Python的轻量级web开发框架Flask,为什么要推荐它呢?当然是因为它够轻量级了,开发迅速是它的特点,当然它也有缺点,不过这里不说,因为既用它又说它差感觉不好。一、安装flaskpip install flask 需要注意的是,还有flake,那个是检查错误的模块,不要搞混了可以看到成功了,总共下载了四个模块,分别是对应的web开