起因
之前是看面试题的时候突然看到前端怎么处理十万条数据,用户的交互是下拉列表的形式展示,觉得有点意思,就自己上手搞一波
面试官的考点
我分析了一波,面试官的考点可能有以下几点:
- 前端渲染海量 dom 节点,用 fragement 创建?
- 海量 dom 节点,浏览器肯定会卡,怎么让用户无感知的进行交互
- 十万条数据怎么接,全部放在内存中?
虚拟列表
首先想到的就是虚拟列表技术,假设在我们的视窗中,最多展示二十条数据,我们只需要创建二十个 dom 节点,只要在用户鼠标滚动的时候去更新 dom 的内容,效果就达成了,直接动手!!!
虚拟列表实现
实现步骤
-
指定一些变量
- length = parseInt(scrollTop / itemHeight) 滚动上去的数据条数,取整
- height 总高度
- total 数据总条数
- itemHeight 单条数据高度
- num 视窗总共展示数据的数量
- start = length 查询数据的起始位置
-
容器滚动条需要一个空的元素撑起来,height = total * itemHeight
-
实际视窗的高度 = num * itemHeight
-
这个时候我们只要在 scroll 事件中拿到 scrollTop,我们就知道了 length,之后更新实际内容 dom 的 marginTop(length * itemHeight),这个时候我们的内容一直在视窗之中
-
上面一步保证了实际内容容器在视窗之中,下面我们就去获取需要展示的数据,也就是 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
可以做海量数据的存储,并且支持检索
参考文档:
实现
搭建服务
首先为了方便预览,用 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()
}
})
}