Skip to main content

判断元素是否在视口内的方法

1. 使用 Intersection Observer API(推荐)

现代浏览器推荐的方式,性能最好,支持异步回调:

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 元素进入视口
console.log("元素在视口中", entry.target);
// 可选:取消观察,如果只需要触发一次
// observer.unobserve(entry.target);
} else {
// 元素离开视口
console.log("元素离开视口", entry.target);
}
});
},
{
// 可选的配置项
root: null, // 默认为视口,可指定为某个滚动容器
rootMargin: "0px", // 提前/延后触发,如 '10px' 或 '0px 0px 50px 0px'
threshold: 0.1, // 0-1 之间,表示元素可见比例达到多少时触发
}
);

// 开始观察目标元素
const target = document.querySelector(".target-element");
observer.observe(target);

2. 使用 getBoundingClientRect()

传统方式,同步计算,可能影响性能:

function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

// 检查元素是否在视口内
return (
rect.top < windowHeight &&
rect.bottom > 0 &&
rect.left < windowWidth &&
rect.right > 0
);
}

// 使用示例
const element = document.querySelector(".target-element");
console.log("元素在视口中:", isInViewport(element));

// 监听滚动事件
window.addEventListener(
"scroll",
() => {
console.log("元素在视口中:", isInViewport(element));
},
{ passive: true }
); // 使用 passive 提高滚动性能

3. 带阈值的检查

检查元素是否在视口中达到一定比例:

function isInViewport(element, threshold = 0.5) {
const rect = element.getBoundingClientRect();
const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

// 计算元素与视口的重叠区域
const visibleHeight =
Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
const visibleWidth =
Math.min(rect.right, windowWidth) - Math.max(rect.left, 0);

if (visibleHeight <= 0 || visibleWidth <= 0) return false;

// 计算可见面积比例
const visibleArea = visibleHeight * visibleWidth;
const elementArea = element.offsetWidth * element.offsetHeight;
const visibleRatio = visibleArea / elementArea;

return visibleRatio >= threshold;
}

4. 使用 Intersection Observer 的 Polyfill

如果需要支持旧浏览器,可以添加 polyfill:

<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>

性能考虑

  1. 优先使用 Intersection Observer:它比监听滚动事件更高效,因为它在浏览器空闲时运行
  2. 节流/防抖:如果必须使用滚动事件,确保添加节流/防抖
  3. 取消观察:当不再需要观察元素时,使用 unobserve()disconnect()
  4. 批量操作:避免在回调中执行重绘/重排操作

实际应用示例

// 懒加载图片
const lazyImages = document.querySelectorAll("img[data-src]");

const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.onload = () => {
img.removeAttribute("data-src");
img.classList.add("loaded");
};
observer.unobserve(img); // 加载后取消观察
}
});
},
{
rootMargin: "200px 0px", // 提前200px加载
}
);

lazyImages.forEach((img) => imageObserver.observe(img));

Hooks

import { useState, useEffect, useRef } from "react";

const useLazyLoad = (options = { rootMargin: "200px 0px" }) => {
const [isInView, setIsInView] = useState(false);
const targetRef = useRef<HTMLElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.unobserve(entry.target);
}
});
},
{
...options,
}
);

const currentRef = targetRef.current;
if (currentRef) {
observer.observe(currentRef);
}

return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [options]);

return [targetRef, isInView] as const;
};

// 使用示例
const MyComponent = () => {
const [lazyRef, isInView] = useLazyLoad();

return (
<div>
<div style={{ height: "1000px" }}>滚动下方</div>
<div
ref={lazyRef}
style={{
height: "200px",
background: isInView ? "lightgreen" : "lightcoral",
}}
>
{isInView ? "元素在视口中!" : "元素不可见"}
</div>
</div>
);
};

CSS 方案

.content {
content-visibility: auto; /* 元素在视口中时才渲染 */
content-intrinsic-size: 100px 100px; /* 预估高度,避免滚动条跳动 */
}