字数 2270,阅读大约需 12 分钟
我们日常工作中,经常需要知道一个元素是不是出现在用户的屏幕里。
比如,图片懒加载需要判断图片是否进入可视区再加载,无限滚动列表需要判断是否滚动到底部来加载更多,或者统计某个广告模块的曝光次数。
今天,我们就来聊聊,用JavaScript判断DOM元素是否在可视区域,有哪些方法。
为什么需要判断元素是否可见?
-
• 性能优化:这是最重要的原因。一个网页可能有几十上百张图片,如果用户一打开页面就全部加载,会浪费流量,页面打开也会很慢。我们只加载用户看得到的图片,这就是“懒加载”。 -
• 交互体验:当用户滚动到某个区域时,我们可能希望触发一些动画,让页面看起来更生动。 -
• 数据统计:产品经理可能想知道,一个重要的按钮或者广告,有多少用户真的看到了它。这就需要记录元素的“曝光”事件。
知道了为什么做,我们来看看怎么做。
基础方法:Element.getBoundingClientRect()
这是最经典、兼容性最好的方法。几乎所有的浏览器都支持它。
getBoundingClientRect() 方法返回一个对象,这个对象提供了元素的大小及其相对于视口(viewport) 的位置。
什么是视口?简单说,就是你现在看到的浏览器窗口的那部分区域,不包括工具栏、地址栏。
返回的对象包含这些属性:
-
• top: 元素上边界到视口顶部的距离 -
• right: 元素右边界到视口左边的距离 -
• bottom: 元素下边界到视口顶部的距离 -
• left: 元素左边界到视口左边的距离 -
• width: 元素的宽度(包含padding和border) -
• height: 元素的高度(包含padding和border)
这里有一个关键点:top、bottom、left、right 的值是相对于视口左上角的。当元素完全在视口上方时,bottom 的值会是负数。
如何判断元素是否在视口内?
根据上面的定义,我们可以推导出判断逻辑。一个元素在视口内,需要同时满足四个条件:
-
1. 元素的顶部 ( top) 在视口底部之上(即top小于视口高度)。 -
2. 元素的底部 ( bottom) 在视口顶部之下(即bottom大于 0)。 -
3. 元素的左侧 ( left) 在视口右侧之左(即left小于视口宽度)。 -
4. 元素的右侧 ( right) 在视口左侧之右(即right大于 0)。
用代码写出来是这样的:
function isElementInViewport(el) {
const rect = el.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
);
}
这个函数检查元素是否完全在视口内。有时候我们的需求更宽松,比如“元素有一部分进入视口就算”。比如懒加载,通常图片刚露出一个头,我们就开始加载了。
修改一下判断逻辑:
function isElementPartiallyInViewport(el) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
// 判断元素是否在垂直方向与视口有交集
const vertInView = rect.top <= windowHeight && rect.bottom >= 0;
// 判断元素是否在水平方向与视口有交集
const horInView = rect.left <= windowWidth && rect.right >= 0;
return vertInView && horInView;
}
优点和缺点
优点:
-
• 兼容性极好,IE6以上都支持。 -
• 使用简单,逻辑清晰。 -
• 返回的信息很全,除了判断可见性,还能知道具体位置。
缺点:
-
• 这是一个同步方法,频繁调用(比如在 scroll事件中)可能引发性能问题,因为它会导致浏览器重新计算样式和布局(重排)。 -
• 需要自己写判断逻辑,虽然不复杂,但容易出错。
现代方法:Intersection Observer API
因为 getBoundingClientRect() 在滚动监听中的性能问题,浏览器推出了一个专门的API来解决这个问题,它就是 Intersection Observer API(交叉观察器)。
你可以把它理解为一个“侦察兵”。你告诉这个侦察兵:“去盯着那个元素,当它进入或离开另一个元素(通常是视口)的时候,回来告诉我。”
基本用法
使用它分为三步:
-
1. 创建一个观察器 (Observer):设定侦察兵的“任务规则”。 -
2. 告诉它观察哪个目标 (Target):让侦察兵去盯住具体的元素。 -
3. 定义回调函数 (Callback):侦察兵回来时,要做什么。
看一个最简单的例子:
// 1. 创建观察器
const observer = new IntersectionObserver((entries, observer) => {
// entries 是一个数组,包含所有被观察元素的信息
entries.forEach(entry => {
// entry.isIntersecting 是核心属性,为 true 表示目标进入视口
if (entry.isIntersecting) {
console.log('元素进入视口了!', entry.target);
// 这里可以执行加载图片、触发动画等操作
// 如果只需要触发一次,可以停止观察
// observer.unobserve(entry.target);
} else {
console.log('元素离开视口了!', entry.target);
}
});
}, {
// 2. 这是可选的配置项
// root: 指定根元素,默认为浏览器视口
// threshold: 阈值,可以是数组 [0, 0.25, 0.5, 0.75, 1]
// rootMargin: 类似于CSS的margin,可以扩大或缩小观察区域
});
// 3. 开始观察目标元素
const targetElement = document.querySelector('#myImage');
observer.observe(targetElement);
配置项详解
创建观察器时的第二个参数是配置对象,很强大:
-
• root: 用来观察的根元素,必须是目标元素的祖先。默认是null,即浏览器视口。 -
• rootMargin: 根元素的边距。比如设置“10px 20px 30px 40px”,相当于把观察区域上下左右各扩大或缩小指定的像素。这个特性非常有用,可以实现“提前加载”。 -
• threshold: 阈值。可以是一个数字,也可以是数组。比如: -
• threshold: 0:目标元素刚刚和根元素有1像素交叉,回调就会触发。 -
• threshold: 1.0:目标元素完全进入根元素区域时,回调才会触发。 -
• threshold: [0, 0.5, 1]:目标元素的可见比例每达到0%、50%、100%时,回调都会触发一次。
一个实用的懒加载例子
假设我们有一组图片,它们的 src 属性放在 data-src 里。
<img class="lazy" data-src="image1.jpg" alt="...">
<img class="lazy" data-src="image2.jpg" alt="...">
<!-- 更多图片... -->
用 Intersection Observer 实现懒加载:
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('img.lazy');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 将 data-src 的值赋给 src,浏览器就会开始加载图片
img.src = img.dataset.src;
img.classList.remove('lazy');
// 图片加载后,停止观察它
observer.unobserve(img);
console.log('懒加载了一张图片:', img.src);
}
});
}, {
// 提前100像素开始加载
rootMargin: '0px 0px 100px 0px'
});
// 开始观察所有懒加载图片
lazyImages.forEach(img => imageObserver.observe(img));
});
优点和缺点
优点:
-
• 性能好:异步执行,不阻塞主线程,也不会因为滚动事件频繁触发而导致性能问题。 -
• 功能强大:可以精确控制触发的时机(通过 threshold),可以指定观察区域(通过root和rootMargin)。 -
• 使用方便:API设计清晰,省去了自己计算位置和绑定滚动事件的麻烦。
缺点:
-
• 兼容性:现代浏览器支持良好,但IE完全不支持。如果需要支持IE,必须使用polyfill。 -
• 学习成本:对于新手来说,概念比 getBoundingClientRect()稍微复杂一点。
如何选择?
两种方法各有优劣,你可以根据项目情况来选择。
|
|
getBoundingClientRect() |
Intersection Observer |
|---|---|---|
| 兼容性 | 极好
|
|
| 性能 |
|
优秀
|
| 功能 |
|
强大
|
| 复杂度 |
|
|
| 适用场景 |
|
|
最优建议:
-
1. 如果你的项目是内部系统,或者明确不需要支持旧版浏览器(如IE),毫不犹豫地选择 Intersection Observer API。它是未来的标准,性能更好,代码更简洁。 -
2. 如果你需要支持IE等老浏览器,并且交互不复杂,那么使用 getBoundingClientRect()是稳妥的选择。记得要配合函数节流(throttle) 使用,避免滚动事件触发太频繁。function throttle(func, wait) {
let timeout = null;
return function() {
if (!timeout) {
timeout = setTimeout(() => {
func.apply(this, arguments);
timeout = null;
}, wait);
}
};
}
// 使用节流后的函数监听滚动
window.addEventListener('scroll', throttle(checkElementsInView, 200)); -
3. 在大型项目中,可以做一个兼容性封装,优先使用 Intersection Observer,不支持时再回退到 getBoundingClientRect+ 节流。
判断DOM元素是否在可视区域,从手动计算位置的 getBoundingClientRect,到浏览器原生支持的 Intersection Observer,我们看到了Web平台的发展。
🚀专注前沿技术拆解 | 每日 9:00 更新
👇 关注 | 点赞 | 分享,我们共同进化
🔥 热门文章推荐:

