1. Intersection Observer API(推荐)
这是现代浏览器推荐的方法,性能最好,异步执行,不会阻塞主线程。
基础用法
javascript
// 创建观察器
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入可视区域', entry.target);
// 可以在这里执行懒加载等操作
entry.target.classList.add('visible');
} else {
console.log('元素离开可视区域', entry.target);
entry.target.classList.remove('visible');
}
});
});
// 观察元素
const elements = document.querySelectorAll('.watch-element');
elements.forEach(el => observer.observe(el));
高级配置
javascript
const options = {
// root: 指定根元素,默认为浏览器视窗
root: null, // 或者指定特定元素,如 document.querySelector('.container')
// rootMargin: 根的外边距,可以扩大或缩小根的边界框
rootMargin: '10px 0px -100px 0px', // 上右下左,类似CSS margin
// threshold: 触发回调的可见比例
threshold: [0, 0.25, 0.5, 0.75, 1] // 在0%, 25%, 50%, 75%, 100%可见时触发
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const visiblePercentage = Math.round(entry.intersectionRatio * 100);
console.log(`元素可见 ${visiblePercentage}%`);
// 根据可见比例执行不同操作
if (entry.intersectionRatio > 0.5) {
// 超过50%可见
entry.target.classList.add('mostly-visible');
}
});
}, options);
实用工具函数
ini
// 封装的工具函数
function createVisibilityObserver(options = {}) {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const finalOptions = { ...defaultOptions, ...options };
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;
const isVisible = entry.isIntersecting;
// 触发自定义事件
element.dispatchEvent(new CustomEvent('visibilityChange', {
detail: { isVisible, entry }
}));
});
}, finalOptions);
}
// 使用示例
const observer = createVisibilityObserver({ threshold: 0.5 });
document.querySelectorAll('.lazy-load').forEach(element => {
observer.observe(element);
element.addEventListener('visibilityChange', (e) => {
if (e.detail.isVisible) {
// 执行懒加载
const img = element.querySelector('img[data-src]');
if (img) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
}
});
});
2. getBoundingClientRect() 方法
传统方法,同步执行,需要手动调用。
基础用法
javascript
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowHeight &&
rect.right <= windowWidth
);
}
// 使用示例
const element = document.querySelector('.target');
if (isInViewport(element)) {
console.log('元素完全在视窗内');
}
部分可见判断
ini
function isPartiallyInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.bottom > 0 &&
rect.top < windowHeight &&
rect.right > 0 &&
rect.left < windowWidth
);
}
// 更详细的可见性信息
function getVisibilityInfo(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
// 计算可见区域
const visibleTop = Math.max(0, rect.top);
const visibleLeft = Math.max(0, rect.left);
const visibleBottom = Math.min(windowHeight, rect.bottom);
const visibleRight = Math.min(windowWidth, rect.right);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleArea = visibleWidth * visibleHeight;
const totalArea = rect.width * rect.height;
return {
isVisible: visibleArea > 0,
isFullyVisible: isInViewport(element),
visibilityRatio: totalArea > 0 ? visibleArea / totalArea : 0,
rect: rect,
visibleArea: { width: visibleWidth, height: visibleHeight }
};
}
// 使用示例
const element = document.querySelector('.target');
const info = getVisibilityInfo(element);
console.log(`可见比例: ${(info.visibilityRatio * 100).toFixed(2)}%`);
滚动监听版本
kotlin
class ScrollVisibilityTracker {
constructor(options = {}) {
this.elements = new Map();
this.threshold = options.threshold || 0.1;
this.throttleDelay = options.throttleDelay || 100;
this.checkVisibility = this.throttle(this.checkVisibility.bind(this), this.throttleDelay);
this.bindEvents();
}
observe(element, callback) {
this.elements.set(element, {
callback,
wasVisible: false
});
// 初始检查
this.checkElement(element);
}
unobserve(element) {
this.elements.delete(element);
}
checkVisibility() {
this.elements.forEach((data, element) => {
this.checkElement(element);
});
}
checkElement(element) {
const data = this.elements.get(element);
if (!data) return;
const info = getVisibilityInfo(element);
const isVisible = info.visibilityRatio >= this.threshold;
if (isVisible !== data.wasVisible) {
data.wasVisible = isVisible;
data.callback(isVisible, info);
}
}
bindEvents() {
window.addEventListener('scroll', this.checkVisibility, { passive: true });
window.addEventListener('resize', this.checkVisibility);
}
destroy() {
window.removeEventListener('scroll', this.checkVisibility);
window.removeEventListener('resize', this.checkVisibility);
this.elements.clear();
}
throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
}
// 使用示例
const tracker = new ScrollVisibilityTracker({ threshold: 0.5 });
document.querySelectorAll('.track-element').forEach(element => {
tracker.observe(element, (isVisible, info) => {
if (isVisible) {
element.classList.add('in-view');
console.log('元素进入视窗', info);
} else {
element.classList.remove('in-view');
}
});
});
3. 特殊场景的解决方案
在滚动容器中的元素
javascript
function isInScrollContainer(element, container) {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return (
elementRect.top >= containerRect.top &&
elementRect.left >= containerRect.left &&
elementRect.bottom <= containerRect.bottom &&
elementRect.right <= containerRect.right
);
}
// 使用Intersection Observer观察滚动容器
function createContainerObserver(container) {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
console.log('容器内元素可见性变化', entry.isIntersecting);
});
}, {
root: container, // 指定容器为根元素
threshold: 0.1
});
}
考虑CSS Transform的情况
arduino
function getTransformedBounds(element) {
const rect = element.getBoundingClientRect();
// 如果元素有CSS transform,getBoundingClientRect已经包含了变换后的位置
// 不需要额外计算
return rect;
}
// 对于复杂的3D变换,可能需要更精确的计算
function isTransformedElementVisible(element) {
const rect = element.getBoundingClientRect();
// 检查元素是否因为transform: scale(0)等而不可见
const computedStyle = getComputedStyle(element);
const transform = computedStyle.transform;
if (transform === 'none') {
return isPartiallyInViewport(element);
}
// 检查是否有scale(0)或类似的变换
if (rect.width === 0 || rect.height === 0) {
return false;
}
return isPartiallyInViewport(element);
}
4. 性能优化技巧
虚拟滚动场景
kotlin
class VirtualScrollObserver {
constructor(container, options = {}) {
this.container = container;
this.itemHeight = options.itemHeight || 100;
this.buffer = options.buffer || 5; // 缓冲区项目数量
this.items = [];
this.visibleRange = { start: 0, end: 0 };
this.handleScroll = this.throttle(this.calculateVisibleRange.bind(this), 16);
this.container.addEventListener('scroll', this.handleScroll);
}
calculateVisibleRange() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
const start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
const end = Math.min(
this.items.length - 1,
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.buffer
);
if (start !== this.visibleRange.start || end !== this.visibleRange.end) {
this.visibleRange = { start, end };
this.onVisibleRangeChange(this.visibleRange);
}
}
onVisibleRangeChange(range) {
// 子类实现或通过回调处理
console.log('可见范围变化:', range);
}
throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
}
懒加载图片完整实现
kotlin
class LazyImageLoader {
constructor(options = {}) {
this.options = {
rootMargin: '50px',
threshold: 0.1,
loadingClass: 'lazy-loading',
loadedClass: 'lazy-loaded',
errorClass: 'lazy-error',
...options
};
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: this.options.rootMargin,
threshold: this.options.threshold
}
);
this.loadingImages = new Set();
}
observe(img) {
if (!(img instanceof HTMLImageElement)) {
console.warn('LazyImageLoader: 只能观察img元素');
return;
}
if (!img.dataset.src && !img.dataset.srcset) {
console.warn('LazyImageLoader: 图片缺少data-src或data-srcset属性');
return;
}
this.observer.observe(img);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
if (this.loadingImages.has(img)) return;
this.loadingImages.add(img);
img.classList.add(this.options.loadingClass);
const tempImg = new Image();
tempImg.onload = () => {
this.applyImage(img, tempImg);
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
this.loadingImages.delete(img);
};
tempImg.onerror = () => {
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
this.loadingImages.delete(img);
};
// 支持srcset
if (img.dataset.srcset) {
tempImg.srcset = img.dataset.srcset;
}
tempImg.src = img.dataset.src;
}
applyImage(img, tempImg) {
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
delete img.dataset.srcset;
}
img.src = tempImg.src;
delete img.dataset.src;
}
destroy() {
this.observer.disconnect();
this.loadingImages.clear();
}
}
// 使用示例
const lazyLoader = new LazyImageLoader({
rootMargin: '100px',
threshold: 0.1
});
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
5. 兼容性处理
ini
// Intersection Observer polyfill检查
function createCompatibleObserver(callback, options) {
if ('IntersectionObserver' in window) {
return new IntersectionObserver(callback, options);
} else {
console.warn('IntersectionObserver not supported, falling back to scroll listener');
return createScrollBasedObserver(callback, options);
}
}
function createScrollBasedObserver(callback, options = {}) {
const elements = new Set();
const threshold = options.threshold || 0;
function checkElements() {
const entries = [];
elements.forEach(element => {
const info = getVisibilityInfo(element);
const isIntersecting = info.visibilityRatio >= threshold;
entries.push({
target: element,
isIntersecting,
intersectionRatio: info.visibilityRatio,
boundingClientRect: info.rect
});
});
if (entries.length > 0) {
callback(entries);
}
}
const throttledCheck = throttle(checkElements, 100);
window.addEventListener('scroll', throttledCheck, { passive: true });
window.addEventListener('resize', throttledCheck);
return {
observe(element) {
elements.add(element);
// 立即检查一次
setTimeout(() => {
const info = getVisibilityInfo(element);
const isIntersecting = info.visibilityRatio >= threshold;
callback([{
target: element,
isIntersecting,
intersectionRatio: info.visibilityRatio,
boundingClientRect: info.rect
}]);
}, 0);
},
unobserve(element) {
elements.delete(element);
},
disconnect() {
window.removeEventListener('scroll', throttledCheck);
window.removeEventListener('resize', throttledCheck);
elements.clear();
}
};
}
总结
选择合适的方法:
- Intersection Observer API - 现代浏览器首选,性能最佳
- getBoundingClientRect + 滚动监听 - 需要兼容老浏览器时使用
- 虚拟滚动 - 处理大量元素时的特殊优化
关键考虑因素:
- 性能:Intersection Observer > 节流的滚动监听 > 频繁的滚动监听
- 精确度:getBoundingClientRect更精确,但需要手动触发
- 兼容性:getBoundingClientRect支持更老的浏览器
- 功能需求:是否需要部分可见、可见比例等详细信息