虚拟滚动的实现
思路:它只渲染当前可视区域内的元素,而不是整个列表,滚动时计算出应该显示哪些元素
原生JS
class VirtualScroll {
constructor(container, items, itemHeight, renderItem) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.renderItem = renderItem;
this.visibleItemCount = Math.ceil(container.clientHeight / itemHeight);
this.totalHeight = items.length * itemHeight;
this.init();
}
init() {
// 设置容器高度以保持正确滚动条
this.container.style.height = `${this.totalHeight}px`;
// 创建内容容器
this.content = document.createElement('div');
this.container.appendChild(this.content);
// 初始渲染
this.render();
// 添加滚动事件监听
this.container.addEventListener('scroll', () => this.render());
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + this.visibleItemCount + 1, // +1 防止滚动时出现空白
this.items.length
);
// 计算内容偏移量
const offsetY = startIndex * this.itemHeight;
// 渲染可见项
let html = '';
for (let i = startIndex; i < endIndex; i++) {
html += this.renderItem(this.items[i], i);
}
this.content.innerHTML = html;
this.content.style.transform = `translateY(${offsetY}px)`;
}
}
// 使用示例
const container = document.getElementById('scroll-container');
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
const itemHeight = 50;
const virtualScroll = new VirtualScroll(
container,
items,
itemHeight,
(item, index) => `
<div style="height: ${itemHeight}px; border-bottom: 1px solid #eee;">
${item}
</div>
`
);
vue
<template>
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">
{{ item.text }}
</div>
</RecycleScroller>
</template>
<script>
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
export default {
components: {
RecycleScroller
},
data() {
return {
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i + 1}`
}))
};
}
};
</script>
<style>
.scroller {
height: 500px;
}
.item {
height: 50px;
border-bottom: 1px solid #eee;
}
</style>
懒加载的实现
原理:延迟加载,例如图片:第一次只显示10张,滚动时请求显示20张,累加的
图片懒加载实现
原生JS
1. 使用原生HTML loading="lazy"属性
<img src="placeholder.jpg" data-src="actual-image.jpg" loading="lazy" alt="示例图片">
2. 使用Intersection Observer API
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
const lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// 回退方案
lazyImages.forEach(function(lazyImage) {
lazyImage.src = lazyImage.dataset.src;
});
}
});
3. 滚动事件监听实现(兼容旧浏览器)
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
const windowHeight = window.innerHeight;
images.forEach(img => {
const imgTop = img.getBoundingClientRect().top;
if (imgTop < windowHeight + 100) { // 提前100px加载
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
}
// 初始加载
lazyLoad();
// 滚动事件节流
window.addEventListener('scroll', throttle(lazyLoad, 200));
function throttle(func, wait) {
let timeout;
return function() {
if (!timeout) {
timeout = setTimeout(() => {
func();
timeout = null;
}, wait);
}
};
}
vue
1.使用动态导入
const LazyComponent = () => import('./HeavyComponent.vue');
new Vue({
components: {
LazyComponent
}
});
2. 结合路由的懒加载
const router = new VueRouter({
routes: [
{
path: '/heavy',
component: () => import('./views/HeavyView.vue')
}
]
});
3. 基于视口的组件懒加载
<template>
<div ref="container">
<component v-if="isVisible" :is="loadedComponent" />
<div v-else>加载中...</div>
</div>
</template>
<script>
export default {
props: {
component: {
type: Promise,
required: true
}
},
data() {
return {
isVisible: false,
loadedComponent: null
};
},
mounted() {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
this.loadComponent();
observer.unobserve(this.$refs.container);
}
},
{ threshold: 0.1 }
);
observer.observe(this.$refs.container);
},
methods: {
async loadComponent() {
this.loadedComponent = (await this.component).default;
this.isVisible = true;
}
}
};
</script>
高并发组件