本文首發在我的個人博客上:JavaScript原生實現簡單虛擬列表(列表不定高)
https://www.brandhuang.com/article/1745637125513
前言
之前實現了一個定高版本的虛擬列表,今天在定高版本的基礎上稍作調整,來實現不定高版本,之前的版本請跳轉對應文章查看:JavaScript原生實現簡單虛擬列表(固定高度)?https://www.brandhuang.com/article/1616725433946。
先說結論
實現不定高的原理就是:每次把內容渲染到頁面后,都去重新獲取一次 item 的實際高度,然后再執行一次渲染。
最開始想用?createDocumentFragment
?創建一個虛擬的文檔節點,先將內容渲染到這個虛擬的文檔節點中,最后從虛擬文檔節點中獲取 item 的實際高度,最后,一次插入到真實DOM中。結果發現,在虛擬的文檔節點中時拿不到 item 的實際高度的,所以才有了下面的實現方式。
完整代碼
不定的高版本代碼如下:
html 和 css 與定高版本相比,未做任何調整
<div class="container"><div class="zhanwei"></div></div><style> .container {border: 1px solid #eee;height: 300px;width: 300px;overflow: auto;position: relative;box-sizing: border-box;}.zhanwei {position: relative;}.item {position: absolute;top: 0;min-height: 50px;width: 100%;border: 1px solid #eee;will-change: transform; box-sizing: border-box;}.item:nth-of-type(odd) {background: #00ccff;}.item:nth-of-type(even) {background: #ffcc00;}</style>
js 代碼如下
和定高版本相比,就兩處改動,請查看代碼中的?變化一
?和?變化二
;
// 不固定高度版本let container = document.querySelector('.container');let zhanwei = document.querySelector('.zhanwei');let itemList = []; // 假設有10000條數據for (let i = 0; i < 10000; i++) {// 生成10000條數據itemList.push({index: i,content: `Item ${i} - ${"Hello world!".repeat(Math.floor(Math.random() * 10))}`});};let buffer = 5; // 多渲染幾條,避免滾動看著異常let itemHeight = 50; // 每條數據的一個默認最小高度let heights = new Map();// 記錄渲染的每個 item 的高度,為不定高版本做準備let offsets = new Map(); // 記錄每個 item 的偏移量,即每個item距離頂部的距離let rendered = new Map(); // 存儲已渲染的數據// 更新偏移量, 根據item高度,計算 zhanwei 元素的高度,好讓container出現滾動條function updateOffsets() {let offset = 0for (let i = 0; i < itemList.length; i++) {let h = heights.get(i) ?? itemHeight; // ?? 是空值合并運算符,當左邊為null或者undefined時使用右邊值,和三元運算符相比,排除了 0 的干擾offsets[i] = offset;offset += h + 5; // 加上了5個像素的間距}zhanwei.style.height = offset + 'px';}// 變化一:創建一個重新渲染函數function rerender(item, i) {let height = item.getBoundingClientRect().heightif (heights.get(itemList[i].index) !== height) {heights.set(itemList[i].index, height)updateOffsets()render()}}// 渲染數據function render() {let scrollTop = container.scrollTop;let viewHeight = container.clientHeight;let start = 0; // 查找視口第一個item的索引while (start < itemList.length && offsets[start + 1] < scrollTop) {start++;}let end = start ;// 查找視口最后一個item的索引while (end < itemList.length && offsets[end] < scrollTop + viewHeight) {end++;}start = Math.max(0, start - buffer);end = Math.min(itemList.length, end + buffer);let nextRendered = new Map(); // 當前需要渲染的數據for (let i = start; i < end; i++) {if (!rendered.has(i)) {let item = document.createElement('div')item.className = 'item'item.style.transform = `translateY(${offsets[itemList[i].index] + 'px'})`item.textContent = itemList[i].contentcontainer.appendChild(item)rendered.set(i, item)// 變化二:向頁面插入數據后執行一次重新渲染rerender(item, i)}nextRendered.set(i, rendered.get(i))}// 不可見的區域 移除for (const [i, el] of rendered.entries()) {if (!nextRendered.has(i)) {container.removeChild(el);}}// 更新 renderedrendered.clear();for (const [i, el] of nextRendered.entries()) {rendered.set(i, el);}}container.addEventListener("scroll", render);updateOffsets()render()
如果你有更好的實現方案,歡迎留言、貼代碼交流。
感謝你的閱讀 ??