注册
web

后端一次给你10万条数据,如何优雅展示,到底考察我什么

前言

大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)

4cc34033138345488065107afcc4adf1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

前置工作

先把前置工作给做好,后面才能进行测试

后端搭建

新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务

没有安装nodemon的同学可以先全局安装npm i nodemon -g

// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
 // 开启Cors
 res.writeHead(200, {
   //设置允许跨域的域名,也可设置*允许所有域名
   'Access-Control-Allow-Origin': '*',
   //跨域允许的请求方法,也可设置*允许所有方法
   "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
   //允许的header类型
   'Access-Control-Allow-Headers': 'Content-Type'
})
 let list = []
 let num = 0

 // 生成10万条数据的list
 for (let i = 0; i < 100000; i++) {
   num++
   list.push({
     src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
     text: `我是${num}号嘉宾林三心`,
     tid: num
  })
}
 res.end(JSON.stringify(list));
}).listen(port, function () {
 console.log('server is listening on port ' + port);
})

前端页面

先新建一个index.html

// index.html

// 样式
<style>
   * {
     padding: 0;
     margin: 0;
  }
   #container {
     height: 100vh;
     overflow: auto;
  }
  .sunshine {
     display: flex;
     padding: 10px;
  }
   img {
     width: 150px;
     height: 150px;
  }
 </style>

// html部分
<body>
 <div id="container">
 </div>
 <script src="./index.js"></script>
</body>

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据

// index.js

// 请求函数
const getList = () => {
   return new Promise((resolve, reject) => {
       //步骤一:创建异步对象
       var ajax = new XMLHttpRequest();
       //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
       ajax.open('get', 'http://127.0.0.1:8000');
       //步骤三:发送请求
       ajax.send();
       //步骤四:注册事件 onreadystatechange 状态改变就会调用
       ajax.onreadystatechange = function () {
           if (ajax.readyState == 4 && ajax.status == 200) {
               //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
               resolve(JSON.parse(ajax.responseText))
          }
      }
  })
}

// 获取container对象
const container = document.getElementById('container')

直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间

9396a045360744e7b73dd626a1456bcd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   list.forEach(item => {
       const div = document.createElement('div')
       div.className = 'sunshine'
       div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
       container.appendChild(div)
  })
   console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了

faeac54d664a40ab97ac75325e0984ff~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       setTimeout(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      }, 0)
  }
   render(page)
   console.timeEnd('列表时间')
}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       // 使用requestAnimationFrame代替setTimeout
       requestAnimationFrame(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

文档碎片的好处

  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       requestAnimationFrame(() => {
           // 创建一个文档碎片
           const fragment = document.createDocumentFragment()
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               // 先塞进文档碎片
               fragment.appendChild(div)
          }
           // 一次性appendChild
           container.appendChild(fragment)
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例

af9e2974b1d84d3c852c0e7f606794cd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
 // 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
 // 当前页数与最大页数的比较
 if (page.value > maxPage.value) return
 const clientHeight = container.value?.clientHeight
 const blankTop = blank.value?.getBoundingClientRect().top
 if (clientHeight === blankTop) {
   // blank出现在视图,则当前页数加1
   page.value++
}
}

onMounted(async () => {
 const res = await getList()
 list.value = res
})
</script>

<template>
 <div id="container" @scroll="handleScroll" ref="container">
   <div class="sunshine" v-for="(item) in showList" :key="item.tid">
     <img :src="item.src" />
     <span>{{ item.text }}</span>
   </div>
   <div ref="blank"></div>
 </div>
</template>

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。

作者:Sunshine_Lin
来源:juejin.cn/post/7031923575044964389

1 个评论

给我10w条?找打呢吧

要回复文章请先登录注册