虚拟长列表的实现
虚拟滚动普通版本
虚拟滚动加载原理是什么
原理
虚拟滚动(Virtual Scrolling)是⼀种性能优化的⼿段,通常⽤于处理⻓列表的显⽰问题。
在传统的滚动加载中,当⾯对成千上万项的⻓列表时,直接在 DOM 中创建并展⽰所有项会导致严重的性能问题,
因为浏览器需要渲染所有的列表项。
核⼼原理:是仅渲染⽤⼾可视范围内的列表项,以此减少 DOM 操作的数量和提⾼性能。
实现虚拟滚动,我们需要:
监听滚动事件,了解当前滚动位置。
根据滚动位置计算当前应该渲染哪些列表项⽬(即在视⼝内的项⽬)。
只渲染那些项⽬,并⽤占位符(⽐如⼀个空的 div)占据其它项⽬应有的位置,保持滚动条⼤⼩不
变。
- 当用户滚动时,重新计算并渲染新的项⽬。
简单实现⼀个虚拟滚动加载
基础版本实现
以下是⼀个简单的虚拟滚动实现的 JavaScript 代码⽰例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .scroll-container { position: relative; height: 200px; width: 200px; background-color: antiquewhite; overflow: scroll; } </style> </head>
<body> <div class="scroll-container"> </div> <script> class VirtualScroll { constructor(container, itemHeight, totalItems, renderCallback) { this.container = container; // 容器元素 this.itemHeight = itemHeight; // 每个项的⾼度 this.totalItems = totalItems; // 总列表项数 this.renderCallback = renderCallback; // 渲染每⼀项的回调函数 this.viewportHeight = container.clientHeight; // 视⼝⾼度 this.bufferSize = Math.ceil(this.viewportHeight / itemHeight) * 3; // 缓冲⼤⼩ this.renderedItems = []; // 已渲染项的数组 this.startIndex = 0; // 当前渲染的开始索引 this.endIndex = this.bufferSize; // 当前渲染的结束索引 container.addEventListener("scroll", () => this.onScroll()); this.update(); }
onScroll() { const scrollTop = this.container.scrollTop; console.log(scrollTop, 'scrollTop'); const newStartIndex = Math.floor(scrollTop / this.itemHeight) - Math.ceil(this.bufferSize / 2); const newEndIndex = newStartIndex + this.bufferSize; if (newStartIndex !== this.startIndex || newEndIndex !== this.endIndex) { this.startIndex = Math.max(0, newStartIndex); this.endIndex = Math.min(this.totalItems, newEndIndex); this.update(); } }
update() { this.container.innerHTML = ""; // 清空已有内容 const totalHeight = this.totalItems * this.itemHeight; // 计算容器的总⾼度 this.container.style.height = `${totalHeight} px`; // 设置容器的总⾼度 // 渲染视⼝内的项 const fragment = document.createDocumentFragment(); for (let i = this.startIndex; i < this.endIndex; i++) { const item = this.renderCallback(i); item.style.top = `${i * this.itemHeight}px`; fragment.appendChild(item); } this.container.appendChild(fragment); } }
// 创建⼀个列表项的函数 function createItem(index) { const item = document.createElement("div"); item.className = "list-item"; item.innerText = `Item${index}`; item.style.position = "absolute"; item.style.width = "100%"; return item; }
// 初始化虚拟滚动 const container = document.querySelector(".scroll-container"); // 容器元素需要预先在HTML中定义 const virtualScroll = new VirtualScroll(container, 30, 100, createItem); </script> </body>
</html>
|
这个例⼦中,我们创建了⼀个 VirtualScroll 类,通过传⼊容器、项⾼度、总项数和渲染回调函数来进⾏初始化。
该类的 update ⽅法⽤于渲染出当前可视范围内部分的项⽬,并将它们放到⽂档碎⽚中,然后⼀次性添加到容器中。
这样可以避免多次直接操作 DOM,减少性能消耗。
当滚动时,onScroll ⽅法将计算新的 startIndex 和 endIndex ,然后调⽤ update ⽅法进⾏更新。
请注意,实际应⽤可能需要根据具体情况调整缓冲区⼤⼩等参数。
如何计算开始索引startIndex和endIndex
1 2 3 4 5 6 7
| //constructor()初始化时候方法 this.startIndex = 0; // 当前渲染的开始索引 this.bufferSize = Math.ceil(this.viewportHeight / itemHeight) * 3; // 缓冲⼤⼩ this.endIndex = this.bufferSize; // 当前渲染的结束索引 //onScroll() 方法计算 startIndex const newStartIndex = Math.floor(scrollTop / this.itemHeight) - this.bufferSize / 2; const newEndIndex = newStartIndex + this.bufferSize;
|
计算步骤分析
1. Math.floor(scrollTop / this.itemHeight)
scrollTop
表示容器元素垂直滚动的距离,即用户当前滚动到的位置。
this.itemHeight
是每个列表项的高度。
Math.floor(scrollTop / this.itemHeight)
计算出当前滚动位置对应的列表项的索引。例如,如果每个列表项高度为 30px,用户滚动了 60px,那么 scrollTop / this.itemHeight
结果为 2,意味着用户滚动到了第 2 个列表项的位置。
2. - this.bufferSize / 2
this.bufferSize
是缓冲大小,它定义了在当前视口上下额外渲染的列表项数量。缓冲的作用是为了在用户滚动时,提前渲染一些即将进入视口的元素,使得滚动过程更加流畅,避免出现空白或闪烁的情况。
- **减去
this.bufferSize / 2
是为了在当前视口上方也渲染一部分列表项,作为预加载的缓冲区域。**例如,如果缓冲大小为 9,那么会在视口上方额外渲染 4 个(9 / 2
向下取整)列表项。
总结
1
| const newStartIndex = Math.floor(scrollTop / this.itemHeight) - this.bufferSize / 2;
|
通过上边代码这样的计算方式,startIndex
不仅考虑了当前视口内的第一个列表项的索引,还考虑了视口上方的缓冲区域,从而确保在用户滚动时,能够平滑地过渡到新的列表项,而不会出现明显的卡顿或空白。
虚拟滚动进阶版本
使⽤ IntersectionObserver 来实现
使⽤ IntersectionObserver 实现虚拟滚动就意味着我们会依赖于浏览器的 API 来观察哪些元素进⼊或离开视⼝(viewport),
⽽⾮直接监听滚动事件。这样我们只需在需要时渲染或回收元素。
以下是⼀个简化版使⽤ IntersectionObserver 来实现虚拟滚动的例⼦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>虚拟长列表</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
#app { max-width: 800px; margin: 0 auto; padding: 20px; }
.virtual-list-container { height: 80vh; overflow-y: auto; border: 1px solid #eee; position: relative; background: #fff; }
.list-item { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; transition: background-color 0.2s; }
.list-item:hover { background-color: #f5f5f5; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #0066ff; margin-right: 15px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; }
.content { flex: 1; }
.title { font-size: 16px; color: #333; margin-bottom: 4px; }
.description { font-size: 14px; color: #666; }
.placeholder { height: 70px; } </style> </head> <body> <div id="app"> <h2 style="margin-bottom: 20px;">虚拟长列表示例</h2> <div class="virtual-list-container" id="container"></div> </div>
<script> class VirtualScroll { constructor(container, itemHeight, totalItems, renderItem) { this.container = container; this.itemHeight = itemHeight; this.totalItems = totalItems; this.renderItem = renderItem; this.observer = null; this.init(); }
init() { // 创建IntersectionObserver this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const target = entry.target; const index = parseInt(target.dataset.index); if (entry.isIntersecting) { // 当占位元素进入视口时,渲染实际内容 target.innerHTML = this.renderItem(index); target.classList.remove('placeholder'); } else { // 当元素离开视口时,恢复为占位元素 target.innerHTML = ''; target.classList.add('placeholder'); } }); }, { root: this.container, rootMargin: '100px 0px', threshold: 0 } );
// 创建所有占位元素 for (let i = 0; i < this.totalItems; i++) { const placeholder = document.createElement('div'); placeholder.className = 'list-item placeholder'; placeholder.dataset.index = i; this.container.appendChild(placeholder); this.observer.observe(placeholder); } }
destroy() { if (this.observer) { this.observer.disconnect(); } this.container.innerHTML = ''; } }
// 定义渲染函数 const renderListItem = (index) => { return ` <div class="avatar">${index}</div> <div class="content"> <div class="title">项目 ${index + 1}</div> <div class="description">这是第 ${index + 1} 个项目的详细描述信息</div> </div> `; };
// 初始化虚拟列表 const container = document.getElementById('container'); const itemHeight = 70; const totalItems = 10000;
new VirtualScroll(container, itemHeight, totalItems, renderListItem); </script> </body> </html>
|
在这⾥我们创建了⼀个 VirtualScroll 类,构造函数接收容器元素、每个项的⾼度、总项⽬数和
⽤于渲染每个项⽬的函数。我们在初始化⽅法中,为每个项⽬创建了⼀个占位符元素,并且向
IntersectionObserver 注册了这些占位元素。
当⼀个占位元素进⼊到视⼝中时,我们就会渲染对应的项,并且将它替换这个占位符。当⼀个项离开
视⼝,我们⼜会将它替换回原来的占位符并取消它的注册。
这种⽅法的优势包括:
不需要绑定滚动事件,防⽌滚动性能问题。
浏览器会⾃动优化观察者的回调。
不需要⼿动计算当前应该渲染的项⽬,当用户快速滚动时也不会遇到空⽩内容。
虚拟滚动不定高实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>不定高虚拟长列表</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
#app { max-width: 800px; margin: 0 auto; padding: 20px; }
.virtual-list-container { height: 80vh; overflow-y: auto; border: 1px solid #eee; position: relative; background: #fff; }
.list-item { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; transition: background-color 0.2s; }
.list-item:hover { background-color: #f5f5f5; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #0066ff; margin-right: 15px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; }
.content { flex: 1; }
.title { font-size: 16px; color: #333; margin-bottom: 4px; }
.description { font-size: 14px; color: #666; }
.placeholder { min-height: 70px; /* 最小高度,避免内容未渲染时布局塌陷 */ } </style> </head> <body> <div id="app"> <h2 style="margin-bottom: 20px;">不定高虚拟长列表示例</h2> <div class="virtual-list-container" id="container"></div> </div>
<script> class VirtualScroll { constructor(container, totalItems, renderItem) { this.container = container; this.totalItems = totalItems; this.renderItem = renderItem; this.observer = null; this.init(); }
init() { // 创建IntersectionObserver this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const target = entry.target; const index = parseInt(target.dataset.index); if (entry.isIntersecting) { // 当占位元素进入视口时,渲染实际内容 const content = this.renderItem(index); target.innerHTML = content; target.classList.remove('placeholder');
// 动态调整占位元素的高度 const height = target.offsetHeight; target.style.height = `${height}px`; } else { // 当元素离开视口时,恢复为占位元素 target.innerHTML = ''; target.classList.add('placeholder'); target.style.height = 'auto'; // 恢复为自适应高度 } }); }, { root: this.container, rootMargin: '100px 0px', threshold: 0 } );
// 创建所有占位元素 for (let i = 0; i < this.totalItems; i++) { const placeholder = document.createElement('div'); placeholder.className = 'list-item placeholder'; placeholder.dataset.index = i; this.container.appendChild(placeholder); this.observer.observe(placeholder); } }
destroy() { if (this.observer) { this.observer.disconnect(); } this.container.innerHTML = ''; } }
// 定义渲染函数 const renderListItem = (index) => { const randomLines = Math.floor(Math.random() * 5) + 1; // 随机行数 const descriptionText = Array(randomLines).fill(`这是第 ${index + 1} 个项目的详细描述信息。`).join('<br>'); return ` <div class="avatar">${index}</div> <div class="content"> <div class="title">项目 ${index + 1}</div> <div class="description">${descriptionText}</div> </div> `; };
// 初始化虚拟列表 const container = document.getElementById('container'); const totalItems = 10000;
new VirtualScroll(container, totalItems, renderListItem); </script> </body> </html>
|
改进点说明:
1、完全动态高度:
offsetHeight是DOM元素的一个只读属性,返回元素的像素高度,包括元素的内容、内边距(padding)和边框(border),但不包括外边距(margin)、::before或::after等伪元素。这个属性返回的是一个整数,表示元素的实际渲染高度,常用于获取元素的真实高度以进行布局计算或动态调整。
- 每个项目的高度由实际内容决定,通过
offsetHeight
动态计算。
- 当项目离开视口时,高度恢复为
auto
,确保下次进入视口时重新计算高度。
2、随机内容:
renderListItem
函数生成随机行数的描述文本,模拟不定高度的项目。
3、占位元素:
- 占位元素的最小高度设置为
70px
,避免内容未渲染时布局塌陷。
- 当项目进入视口时,占位元素的高度被动态调整为实际内容的高度。
4、性能优化:
- 使用
IntersectionObserver
监听项目的可见性,避免频繁的滚动监听。
- 仅渲染可见区域的项目,减少 DOM 操作。
虚拟滚动不定高实现出现上下白屏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>不定高虚拟长列表</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
#app { max-width: 800px; margin: 0 auto; padding: 20px; }
.virtual-list-container { height: 80vh; overflow-y: auto; border: 1px solid #eee; position: relative; background: #fff; }
.list-item { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; transition: background-color 0.2s; }
.list-item:hover { background-color: #f5f5f5; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #0066ff; margin-right: 15px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; }
.content { flex: 1; }
.title { font-size: 16px; color: #333; margin-bottom: 4px; }
.description { font-size: 14px; color: #666; }
.placeholder { min-height: 70px; /* 最小高度,避免内容未渲染时布局塌陷 */ } </style> </head> <body> <div id="app"> <h2 style="margin-bottom: 20px;">不定高虚拟长列表示例</h2> <div class="virtual-list-container" id="container"></div> </div>
<script> class VirtualScroll { constructor(container, totalItems, renderItem) { this.container = container; this.totalItems = totalItems; this.renderItem = renderItem; this.observer = null; this.init(); }
init() { // 创建IntersectionObserver,扩大监听范围 this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const target = entry.target; const index = parseInt(target.dataset.index);
if (entry.isIntersecting) { // 当占位元素进入视口时,渲染实际内容 const content = this.renderItem(index); target.innerHTML = content; target.classList.remove('placeholder');
// 动态调整占位元素的高度 const height = target.offsetHeight; target.style.height = `${height}px`; } else { // 当元素离开视口时,恢复为占位元素 target.innerHTML = ''; target.classList.add('placeholder'); target.style.height = 'auto'; // 恢复为自适应高度 } }); }, { root: this.container, rootMargin: '200px 0px', // 扩大监听范围,预渲染更多内容 threshold: 0 } );
// 使用文档片段批量插入占位元素 const fragment = document.createDocumentFragment(); for (let i = 0; i < this.totalItems; i++) { const placeholder = document.createElement('div'); placeholder.className = 'list-item placeholder'; placeholder.dataset.index = i; fragment.appendChild(placeholder); } this.container.appendChild(fragment);
// 监听所有占位元素 const placeholders = this.container.querySelectorAll('.placeholder'); placeholders.forEach(placeholder => this.observer.observe(placeholder)); }
destroy() { if (this.observer) { this.observer.disconnect(); } this.container.innerHTML = ''; } }
// 定义渲染函数 const renderListItem = (index) => { const randomLines = Math.floor(Math.random() * 5) + 1; // 随机行数 const descriptionText = Array(randomLines).fill(`这是第 ${index + 1} 个项目的详细描述信息。`).join('<br>'); return ` <div class="avatar">${index}</div> <div class="content"> <div class="title">项目 ${index + 1}</div> <div class="description">${descriptionText}</div> </div> `; };
// 初始化虚拟列表 const container = document.getElementById('container'); const totalItems = 10000;
new VirtualScroll(container, totalItems, renderListItem); </script> </body> </html>
|
优化点说明:
扩大监听范围:
- 通过设置
rootMargin: '200px 0px'
,提前渲染上下各 200px 的内容,避免快速滚动时出现白屏。
批量插入 DOM:
- 使用
DocumentFragment
批量插入占位元素,减少频繁的 DOM 操作。
高度计算优化:
- 在渲染内容后,立即计算并设置占位元素的高度,确保高度准确。
性能优化:
通过以上优化,白屏问题应该会得到显著改善。如果仍有问题,可以进一步调整 rootMargin
或使用 ResizeObserver
动态监听高度变化。
虚拟滚动最终优化
为了进一步优化不定高度的虚拟长列表,我们可以结合以下策略来提升性能和用户体验:
1. 使用 ResizeObserver
动态监听高度变化
- 当项目内容高度发生变化时,动态调整占位元素的高度,避免布局错乱。
2. 增加缓存机制
3. 分帧渲染
- 使用
requestAnimationFrame
分帧渲染内容,避免一次性渲染过多内容导致卡顿。
4. 优化占位元素
- 使用
transform: translateY
动态调整占位元素的位置,减少布局计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
| <!DOCTYPE html> <html lang="zh-CN">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>不定高虚拟长列表</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
#app { max-width: 800px; margin: 0 auto; padding: 20px; }
.virtual-list-container { height: 80vh; overflow-y: auto; border: 1px solid #eee; position: relative; background: #fff; }
.list-item { padding: 10px 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; transition: background-color 0.2s; }
.list-item:hover { background-color: #f5f5f5; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #0066ff; margin-right: 15px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; }
.content { flex: 1; }
.title { font-size: 16px; color: #333; margin-bottom: 4px; }
.description { font-size: 14px; color: #666; }
.placeholder { min-height: 70px; /* 最小高度,避免内容未渲染时布局塌陷 */ } </style> </head>
<body> <div id="app"> <h2 style="margin-bottom: 20px;">不定高虚拟长列表示例</h2> <div class="virtual-list-container" id="container"></div> </div>
<script> class VirtualScroll { constructor(container, totalItems, renderItem) { this.container = container; this.totalItems = totalItems; this.renderItem = renderItem; this.observer = null; this.resizeObserver = null; this.cache = new Map(); // 缓存已渲染的内容 this.init(); }
init() { // 创建IntersectionObserver,扩大监听范围 this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const target = entry.target; const index = parseInt(target.dataset.index);
if (entry.isIntersecting) { // 当占位元素进入视口时,渲染实际内容 this.renderContent(target, index); } else { // 当元素离开视口时,恢复为占位元素 this.clearContent(target); } }); }, { root: this.container, rootMargin: '800px 0px', // 扩大监听范围,预渲染更多内容 threshold: 0 } );
// 创建ResizeObserver,监听高度变化 this.resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { const target = entry.target; const index = parseInt(target.dataset.index); const height = target.offsetHeight; target.style.height = `${height}px`; }); });
// 使用文档片段批量插入占位元素 const fragment = document.createDocumentFragment(); for (let i = 0; i < this.totalItems; i++) { const placeholder = document.createElement('div'); placeholder.className = 'list-item placeholder'; placeholder.dataset.index = i; fragment.appendChild(placeholder); } this.container.appendChild(fragment);
// 监听所有占位元素 const placeholders = this.container.querySelectorAll('.placeholder'); placeholders.forEach(placeholder => this.observer.observe(placeholder)); }
renderContent(target, index) { if (this.cache.has(index)) { // 如果缓存中有内容,直接使用缓存 target.innerHTML = this.cache.get(index); target.classList.remove('placeholder'); this.resizeObserver.observe(target); // 监听高度变化 return; }
// 使用 requestAnimationFrame 分帧渲染 requestAnimationFrame(() => { const content = this.renderItem(index); target.innerHTML = content; target.classList.remove('placeholder'); this.cache.set(index, content); // 缓存内容 this.resizeObserver.observe(target); // 监听高度变化 }); }
clearContent(target) { target.innerHTML = ''; target.classList.add('placeholder'); target.style.height = 'auto'; // 恢复为自适应高度 this.resizeObserver.unobserve(target); // 停止监听高度变化 }
destroy() { if (this.observer) { this.observer.disconnect(); } if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.container.innerHTML = ''; this.cache.clear(); } }
// 定义渲染函数 const renderListItem = (index) => { const randomLines = Math.floor(Math.random() * 5) + 1; // 随机行数 const descriptionText = Array(randomLines).fill(`这是第 ${index + 1} 个项目的详细描述信息。`).join('<br>'); return ` <div class="avatar">${index}</div> <div class="content"> <div class="title">项目 ${index + 1}</div> <div class="description">${descriptionText}</div> </div> `; };
// 初始化虚拟列表 const container = document.getElementById('container'); const totalItems = 10000;
new VirtualScroll(container, totalItems, renderListItem); </script> </body>
</html>
|
优化点说明:
IntersectionObserver
:
- 监听占位元素的可见性,扩大监听范围(
rootMargin: '200px 0px'
),提前渲染内容,避免白屏。
ResizeObserver
:
- 动态监听项目内容的高度变化,确保占位元素的高度与实际内容一致。
- 缓存机制:
- 分帧渲染:
参考文档:渲染十万条数据解决方案 | 五年前端三年面试