判断元素是否在可视区域
约 1095 字大约 4 分钟
2025-11-09
“判断元素是否在可视区域”的各种实现方案
方案总览
| 分类 | 技术点 | 特点 | 推荐度 |
|---|---|---|---|
| IntersectionObserver | 浏览器原生 API,观察元素与视口的交叉状态 | 性能好、最现代、推荐使用 | ⭐⭐⭐⭐⭐ |
| getBoundingClientRect 手动判断 | 自行计算元素位置与视口边界 | 原理直观,兼容性好 | ⭐⭐⭐⭐ |
| 滚动事件 + 节流 | 监听 scroll 事件,每次滚动判断位置 | 控制力强但性能较差 | ⭐⭐(老方案) |
延伸方案:
| 扩展 | 简介 |
|---|---|
虚拟列表库(如 vue-virtual-scroller、react-window) | 通过“容器高度 + 偏移计算”来判断可视区域 |
| MutationObserver + IntersectionObserver | 动态 DOM 变化时重新观察 |
| requestAnimationFrame 轮询检测 | 少量场景下用于自定义动画或高精度判断 |
IntersectionObserver
浏览器提供的专门 API,用于“观察目标元素是否进入或离开视口(viewport)”。
const target = document.querySelector('#demo')
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('进入视口')
} else {
console.log('离开视口')
}
})
}, {
root: null, // 观察视口(null表示浏览器视窗)
rootMargin: '0px', // 可扩展视口边界(如提前加载)
threshold: 0.1 // 元素可见比例达到 10% 时触发
})
observer.observe(target)特点
- 高性能(浏览器底层优化,不会频繁触发)。
- 支持提前加载(
rootMargin)。 - 异步触发,不阻塞主线程。
- 推荐用于懒加载图片、文档分片渲染、无限滚动。
注意
- 在旧浏览器(IE)不支持,但现代浏览器都支持。
- 某些低端设备滚动频繁时触发稍有延迟。
getBoundingClientRect
直接通过 DOM API 计算元素相对视口的位置。
function isInViewport(el) {
const rect = el.getBoundingClientRect()
return (
rect.top < window.innerHeight &&
rect.bottom > 0
)
}
window.addEventListener('scroll', () => {
const el = document.querySelector('#demo')
if (isInViewport(el)) {
console.log('在可视区域')
}
})getBoundingClientRect() 是 DOM 元素的一个方法,用来获取元素相对于视口(viewport)的位置和大小信息。
它返回一个 DOMRect 对象,包含以下常用属性:
| 属性 | 含义 | 举例说明 |
|---|---|---|
top | 元素顶部相对视口顶部的距离(单位 px) | 如果 top = 0 表示刚好贴着浏览器顶部 |
bottom | 元素底部相对视口顶部的距离 | 如果 bottom = window.innerHeight 表示刚好到底部 |
left | 元素左边相对视口左边的距离 | 水平方向位置 |
right | 元素右边相对视口左边的距离 | |
width | 元素宽度 | 等价于 el.offsetWidth(但更精确) |
height | 元素高度 | 等价于 el.offsetHeight |
window.innerHeight 表示 浏览器视口的高度(单位:像素)。
即用户当前可见区域的高度,不包括滚动条以外的内容。
相关还有:
window.innerWidth→ 浏览器可视宽度。document.documentElement.clientHeight→ 旧浏览器中等价替代。
return (
rect.top < window.innerHeight &&
rect.bottom > 0
)这两行是判断“元素是否出现在视口内”的关键。
| 条件 | 含义 |
|---|---|
rect.top < window.innerHeight | 元素的顶部位置在视口底部之上(即元素上边还没完全滚出下方) |
rect.bottom > 0 | 元素的底部在视口顶部之下(即元素还没完全滚出上方) |
换句话说,只要元素的任意部分在视口内,就返回 true。
特点
- 原理简单直观。
- 可以自己控制判断逻辑(比如提前加载、局部滚动容器判断)。
- 可在任何场景使用(包括老浏览器)。
缺点
- 滚动时每次都计算位置 → 频繁触发会影响性能。
- 需要自己节流/防抖。
优化
可以用 lodash.throttle 或手写节流函数优化:
let timer = null
window.addEventListener('scroll', () => {
if (timer) return
timer = setTimeout(() => {
timer = null
// 计算逻辑
}, 100)
})scroll 事件 + offsetTop 判断
经典的老派方式,依赖 element.offsetTop + window.scrollY。
function isInViewport(el) {
const scrollTop = window.scrollY
const viewportHeight = window.innerHeight
const elTop = el.offsetTop
const elBottom = elTop + el.offsetHeight
return elBottom > scrollTop && elTop < scrollTop + viewportHeight
}特点
- 不用
getBoundingClientRect。 - 原理简单,逻辑可控。
- 缺点同样是性能差,滚动频繁时需要节流。
虚拟列表(Virtual Scrolling)原理判断
比如 vue-virtual-scroller、react-window 用到的思想。
核心思路不是“观察每个元素”,而是:
- 根据滚动距离(scrollTop)和容器高度计算当前可见索引;
- 只渲染该索引范围内的项目。
思想示例:
const visibleStart = Math.floor(scrollTop / itemHeight)
const visibleEnd = visibleStart + Math.ceil(viewportHeight / itemHeight)这种方式性能极高(常用于上万行的长列表),
但适合结构统一的内容(如表格、列表),不太适合 Markdown 这种高度不固定的内容。