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

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


前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了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同理).


初级工程师的方案



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

代码可能是这样的:


  1. 请求后端数据:


fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
  if(res.state) {
    data = res.data
    setList(data)
  }
})
  1. 渲染页面
{
    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>
    })
}
  1. 搜索数据
const handleSearch = (v) => {
    let searchData = data.filter((item, i) => {
      return item.title.indexOf(v) > -1
     })
     setList(searchData)
  }

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


中级工程师的方案



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


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


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


这里我们通过监听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来存储. 效果如下:



搜索后:



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


高级工程师的方案



作为一名久经战场的程序员, 我们应该考虑更优雅的实现方式,比如组件化, 算法优化, 多线程这类问题, 就比如我们问题中的大数据渲染, 我们也可以用虚拟长列表来更优雅简洁的来解决我们的需求. 至于虚拟长列表的实现笔者在开头已经点过,这里就不详细介绍了, 对于更大量的数据,比如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的通信机制来通知主进程, 比如模糊搜索等, 我们还可以对搜索算法进一步优化,比如二分法等,所以这些都是高级工程师该考虑的问题. 但是一定要分清场景, 寻找出性价比更高的方案.



目录
相关文章
|
14小时前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
70 1
|
6天前
|
监控 JavaScript 前端开发
前端的混合之路Meteor篇(六):发布订阅示例代码及如何将Meteor的响应数据映射到vue3的reactive系统
本文介绍了 Meteor 3.0 中的发布-订阅模型,详细讲解了如何在服务器端通过 `Meteor.publish` 发布数据,包括简单发布和自定义发布。客户端则通过 `Meteor.subscribe` 订阅数据,并使用 MiniMongo 实现实时数据同步。此外,还展示了如何在 Vue 3 中将 MiniMongo 的 `cursor` 转化为响应式数组,实现数据的自动更新。
|
6天前
|
JSON 分布式计算 前端开发
前端的全栈之路Meteor篇(七):轻量的NoSql分布式数据协议同步协议DDP深度剖析
本文深入探讨了DDP(Distributed Data Protocol)协议,这是一种在Meteor框架中广泛使用的发布/订阅协议,支持实时数据同步。文章详细介绍了DDP的主要特点、消息类型、协议流程及其在Meteor中的应用,包括实时数据同步、用户界面响应、分布式计算、多客户端协作和离线支持等。通过学习DDP,开发者可以构建响应迅速、适应性强的现代Web应用。
|
27天前
|
JSON 前端开发 Java
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
文章介绍了Java后端如何使用Spring Boot框架响应不同格式的数据给前端,包括返回静态页面、数据、HTML代码片段、JSON对象、设置状态码和响应的Header。
93 1
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
|
19天前
|
JavaScript 前端开发 Python
django接收前端vue传输的formData图片数据
django接收前端vue传输的formData图片数据
19 4
|
18天前
|
前端开发 小程序 Java
java基础:map遍历使用;java使用 Patten 和Matches 进行正则匹配;后端传到前端展示图片三种情况,并保存到手机
这篇文章介绍了Java中Map的遍历方法、使用Pattern和matches进行正则表达式匹配,以及后端向前端传输图片并保存到手机的三种情况。
15 1
|
14小时前
|
JSON Dart 数据格式
<大厂实战场景> ~ flutter&鸿蒙next处理后端返回来的数据的转义问题
在 Flutter 应用开发中,处理后端返回的数据是常见任务,尤其涉及转义字符时。本文详细探讨了如何使用 Dart 的 `dart:convert` 库解析包含转义字符的 JSON 数据,并提供了示例代码和常见问题的解决方案,帮助开发者有效处理数据转义问题。
86 0
|
25天前
|
JavaScript 前端开发
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
本文提供了一个Vue 3教程,讲解了如何使用axios库手动从后端获取数据,包括安装axios、配置后端访问地址、编写路由地址、发起HTTP请求以及在组件中读取和打印响应数据的步骤。
143 0
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
|
26天前
|
存储 前端开发 Java
验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)
本文介绍了使用Kaptcha插件在SpringBoot项目中实现验证码的生成和验证,包括后端生成验证码、前端展示以及通过session进行验证码校验的完整前后端代码和配置过程。
25 0
验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)
|
6天前
|
NoSQL 前端开发 MongoDB
前端的全栈之路Meteor篇(三):运行在浏览器端的NoSQL数据库副本-MiniMongo介绍及其前后端数据实时同步示例
MiniMongo 是 Meteor 框架中的客户端数据库组件,模拟了 MongoDB 的核心功能,允许前端开发者使用类似 MongoDB 的 API 进行数据操作。通过 Meteor 的数据同步机制,MiniMongo 与服务器端的 MongoDB 实现实时数据同步,确保数据一致性,支持发布/订阅模型和响应式数据源,适用于实时聊天、项目管理和协作工具等应用场景。