处理超长的数据

569 阅读4分钟

image.png

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情


超长的数组

如果在面试的时候被问到:“后端一次性返回10000条数据,前端要怎么渲染呢”,这个时候先稳住,先不要拔出你那40米的大刀,让我们慢慢分析分析。

如果直接把这10000条数据直接去渲染

const renderList = async () => {
    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)
    })
}
renderList()

一次渲染 100,000 条记录大约需要 12 秒,这显然是不可取的。

我们再试下用 setTimeout ?

首先想到的应该是分批处理,比如每次处理10条或者20条,然后处理的逻辑放到setTimeout里面,这样不会阻塞主逻辑和页面渲染。

const renderList = async () => {
    const list = await getList()
    // 总数据
    const total = list.length
    // 当前分页下标
    const page = 0
    // 每次处理数据
    const limit = 200
    const totalPage = Math.ceil(total / limit)

    // 处理元素插入
    const render = (page) => {
        if (page >= totalPage) return

        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)
}

还有requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()

加上虚拟列表试试?

其核心思想就是在处理用户滚动时,只改变列表在可视区域的渲染部分,具体步骤为:

先计算可见区域起始数据的索引值startIndex和当前可见区域结束数据的索引值endIndex,假如元素的高度是固定的,那么startIndex的算法很简单,即startIndex = Math.floor(scrollTop/itemHeight)endIndex = startIndex + (clientHeight/itemHeight) - 1,再根据startIndexendIndex取相应范围的数据,渲染到可视区域,然后再计算startOffset(上滚动空白区域)和endOffset(下滚动空白区域),这两个偏移量的作用就是来撑开容器元素的内容,从而起到缓冲的作用,使得滚动条保持平滑滚动,并使滚动条处于一个正确的位置

  • 不把长列表数据一次性全部直接渲染在页面上
  • 截取长列表一部分数据用来填充可视区域
  • 长列表数据不可视部分使用空白占位填充(下图中的startOffsetendOffset区域)
  • 监听滚动事件根据滚动位置动态改变可视列表
  • 监听滚动事件根据滚动位置动态改变空白填充
  1. 计算当前可见区域起始数据的 startIndex
  2. 计算当前可见区域结束数据的 endIndex
  3. 计算当前可见区域的数据,并渲染到页面中
  4. 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset ,并设置到列表上

但是虚拟列表有个问题就是滚动太快的时候js正在计算,出现空白区域。

有没有插件可以实现?

有一个三方库 Vue Virtual Scroll List,它在 Github 上又 2.5k+ 的 stars。

大量的 DOM 元素会使得我们的网页非常“重”。当 DOM 元素超过 1500 至 2000 个的时候,页面就开始又延迟,尤其是在小型的、性能差的设备上尤为明显。

想象一下,有一个无线滚动的页面,你不断的下拉,它实际上可能形成了上万个 DOM 元素,每个元素还包含子节点,这样将消耗巨大的性能。

Virtual scrollers 正是来解决这个问题的。

如上图,已经表示的很清楚了。列表分为可见区域和缓冲区域,超出这个范围的列表 DOM 都将被删除。

试下vue-virtual-scroll-list的效果如何

npm install vue-virtual-scroll-list --save

Root component:

<template>
  <div>
    <virtual-list style="height: 360px; overflow-y: auto;" // make list scrollable
      :data-key="'uid'"
      :data-sources="items"
      :data-component="itemComponent"
    />
  </div>
</template>

<script>
  import Item from './Item'
  import VirtualList from 'vue-virtual-scroll-list'

  export default {
    name: 'root',
    data () {
      return {
        itemComponent: Item,
        items: [{uid: 'unique_1', text: 'abc'}, {uid: 'unique_2', text: 'xyz'}, ...]
      }
    },
    components: { 'virtual-list': VirtualList }
  }
</script>

Item component:

<template>
  <div>{{ index }} - {{ source.text }}</div>
</template>

<script>
  export default {
    name: 'item-component',
    props: {
      index: { // index of current item
        type: Number
      },
      source: { // here is: {uid: 'unique_1', text: 'abc'}
        type: Object,
        default () {
          return {}
        }
      }
    }
  }
</script>

可以看到使用了虚拟列表之后,前端处理后端传过来的大量数据就变得轻量很多,页面也不会有很重的感觉。