阅读 2626
(虚拟列表 + webWorker + indexedDB)处理十万条数据

(虚拟列表 + webWorker + indexedDB)处理十万条数据

起因

之前是看面试题的时候突然看到前端怎么处理十万条数据,用户的交互是下拉列表的形式展示,觉得有点意思,就自己上手搞一波

面试官的考点

我分析了一波,面试官的考点可能有以下几点:

  1. 前端渲染海量 dom 节点,用 fragement 创建?
  2. 海量 dom 节点,浏览器肯定会卡,怎么让用户无感知的进行交互
  3. 十万条数据怎么接,全部放在内存中?

虚拟列表

首先想到的就是虚拟列表技术,假设在我们的视窗中,最多展示二十条数据,我们只需要创建二十个 dom 节点,只要在用户鼠标滚动的时候去更新 dom 的内容,效果就达成了,直接动手!!!

虚拟列表实现

GIF 2021-7-30 9-57-07.gif

实现步骤

image.png

  1. 指定一些变量

    • length = parseInt(scrollTop / itemHeight) 滚动上去的数据条数,取整
    • height 总高度
    • total 数据总条数
    • itemHeight 单条数据高度
    • num 视窗总共展示数据的数量
    • start = length 查询数据的起始位置
  2. 容器滚动条需要一个空的元素撑起来,height = total * itemHeight

  3. 实际视窗的高度 = num * itemHeight

  4. 这个时候我们只要在 scroll 事件中拿到 scrollTop,我们就知道了 length,之后更新实际内容 dom 的 marginTop(length * itemHeight),这个时候我们的内容一直在视窗之中

  5. 上面一步保证了实际内容容器在视窗之中,下面我们就去获取需要展示的数据,也就是 length 到 length + num 之前的数据,然后更新上去就 ok 了

代码:

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <script src="https://unpkg.com/vue@next"></script>
    <title>虚拟列表</title>
    <style>
      .container {
        display: flex;
        border: 1px solid;
        overflow-y: auto;
        height: var(--containerHeight);
      }
      .scroll-blank {
        height: var(--height);
      }
      .scroll {
        margin-top: var(--marginTop);
      }
      .scroll-item {
        height: var(--itemHeight);
        /* background-color: pink; */
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div
        ref='container'
        class="container" 
        :style="{
          '--containerHeight': containerHeight + 'px',
          '--height': height + 'px',
          '--itemHeight': itemHeight + 'px',
          '--marginTop': marginTop + 'px',
        }"
      >
        <div class="scroll-blank"></div>
        <div class="scroll">
          <div v-for='(item, index) in activeList' :key='item' class="scroll-item">{{ item }}</div>
        </div>
      </div>
    </div>
    <script>
      const { computed, onMounted, ref } = Vue

      const createList = () => {
        const list = []

        let i = 0
        while (i < 1000) {
          list.push(i++)
        }

        return list
      }
      const App = {
        setup() {
          const container = ref(null)
          const list = createList()
          const num = 20
          const itemHeight = 20
          const containerHeight = num * itemHeight
          const height = list.length * itemHeight
          let start = ref(0)
          let marginTop = ref(0)

          const activeList = computed(() => {
            const index = start.value
            return list.slice(index, index + 20)
          })

          onMounted(() => {
            container.value.addEventListener('scroll', ({ target }) => {
              const { scrollTop } = target
              const itemNum = scrollTop / itemHeight
              start.value = parseInt(itemNum)
              marginTop.value = scrollTop
            })
          })

          return {
            container,
            marginTop,
            containerHeight,
            height,
            itemHeight,
            activeList
          }
        }
      };

      const app = Vue.createApp(App);
      app.mount("#app");
    </script>
  </body>
</html>
复制代码

十万条数据存放与检索

上面用 Vue3 简单实现了虚拟列表,剩下的就是数据怎么去存放,检索。如果海量数据全部放在主线程的内存中,就感觉有一丝裂开,然后就想到了 webWorker 和 indexedDB 这两个东西!

webWorker

启动一个主线程之外的线程,接收海量数据,不影响主线程

参考文档:

indexedDB

可以做海量数据的存储,并且支持检索

参考文档:

实现

GIF 2021-7-30 11-37-48.gif

搭建服务

首先为了方便预览,用 koa 先搭建一个服务,后面也好调试

const Koa = require('koa')
const Router = require('koa-router');
const fs = require('fs/promises')

const app = new Koa()
const router = new Router()

// 实现的虚拟列表
router.get('/', async (ctx, next) => {
  const content = await fs.readFile('./src/index.html', { encoding: 'utf8' })
  ctx.body = content
})

// worker
router.get('/worker.js', async (ctx, next) => {
  const content = await fs.readFile('./src/worker.js')
  ctx.body = content
})

app.use(router.routes()).use(router.allowedMethods()).listen(9527)

console.log(`预览:`, `\x1B[36mhttp://localhost:9527\x1B[0m`)
复制代码

worker 部分代码

// 创建数据库
const createDatabase = async () => {
  let version = 1
  const databases = await indexedDB.databases()
  const preDatabase = databases.find(({ name }) => name === databaseName)
  if (preDatabase) version = preDatabase.version + 1

  // indexedDB.deleteDatabase(databaseName)
  const database = indexedDB.open(databaseName, version)
  return new Promise((f, r) => {
    database.onsuccess = e => controlDatabase(e.target.result)
    database.onupgradeneeded = f
    database.onerror = r
  })
}

// 创建表
const createTable = db => {
  return new Promise((f, r) => {
    db.deleteObjectStore('list')
    const table = db.createObjectStore('list', { keyPath: 'id' })
    
    table.transaction.oncomplete = () => {
      const store = db.transaction('list', 'readwrite').objectStore('list')
    
      let i = 0
      while (i < 100000) {
        store.add({ id: i++, num: Math.random() })
      }

      f(i)
      isFinished = true
    }
  })
}

// 检索
const search = ({ size, start }) => {
  if (!isFinished) return []
  
  return new Promise((f, r) => {
    const store = db.transaction('list', 'readonly').objectStore('list')
    const range = IDBKeyRange.bound(start, start + size)
    const list = []
    
    store.openCursor(range).onsuccess = ({ target: { result } }) => {
      if (!result) {
        return f(list)
      }

      list.push(result.value)
      result.continue()
    }
  })
}
复制代码

完整代码地址

文章分类
前端
文章标签