判断元素是否在可视区域内

概述

对过长的列表内容优化的方法之一是将看不见的项目隐藏,要实现这个需求必须先判断元素是否可见,判断的方式有多种,大致上可以分成通用的方便的

在分析这个问题之前,先得了解元素尺寸相关的属性,本文将从基础开始讲解。

尺寸相关属性

尺寸属性可以大致分成三类,分别是 offset、client 和 scroll,每一个元素都有这 3 种尺寸属性,每一种的意义都不一样。

offset

offset 多和 offsetParent 相关联,offsetParent 指的是离该元素最近的外层已定位元素(后文简称”定位元素“),若是不存在这样的外层元素,则指向 body。

offsetTop

指的是该元素与定位元素在垂直方向上的距离,是从定位元素的边框内部到该元素的边框外部,也就是说相当于定位元素的 padding-top 加上该元素的 margin-top

要注意的是这个属性是不会改变的,即使该元素被滚动到容器外,取值也不会有变化。

offsetLeft

offsetTop 类似,指的是该元素与定位元素水平方向上的距离,是从定位元素的边框内部到该元素的边框外部,也就是说相当于定位元素的 padding-left 加上该元素的 margin-left

同样不会被改变。

offsetWidth

这个倒是和定位元素没什么关系,指的是该元素从 border-leftborder-right 的距离,包括了 padding 和 content 的宽度。

offsetHeight

同理,这个指的是该元素从 border-topborder-bottom 的距离,包括了 padding 和 content 的高度。

##client

client 指的多是元素本身可见部分(border、padding 和 content),在左右边框宽度相等的前提下,clientLeft * 2 + clientWidth === 元素可见部分宽度

clientTop

指的是该元素 border-top 的高度。

clientLeft

指的是该元素 border-left 的宽度。

clientWidth

指的是该元素 padding 和 content 的宽度总和

clientHeight

指的是该元素 padding 和 content 的高度总和

scroll

scroll 指的多是和滚动相关的尺寸。

scrollTop

指的是该元素的上方滚动的距离,简单来说就是上方隐藏区域的高度,如果没有滚动或不存在滚动条,那么值就是0。

要注意的是,这个的取值是会随着滚动的距离发生变化的。

scrollLeft

指的是该元素的左方滚动的距离,简单来说就是左方隐藏区域的高度,如果没有滚动或不存在滚动条,那么值就是0。

同样会发生变化。

scrollWidth

指的是元素的真实宽度,也就是算上了左右两侧滚动区内容的宽度。

scrollHeight

指的是元素的真实高度,也就是算上了左右两侧滚动区内容的高度。

一图总结

WX20190614-182830@2x.png

判断可视区域

判断的方法有多种,首先是基于尺寸相关属性的判断方法。

由于 offsetTop 是不变的,所以无法直接通过它来取得元素当前的位置,判断元素有没有出上边缘可以通过 el.offsetTop - container.scrollTop 进行,因为 offsetTop 记录了元素与容器的初始相距距离scrollTop 反映了容器上方滚动出去的高度,相减后结果大于 -el.clientHeight 时可以判断出元素还有一部分处在容器内

下边缘也能通过同样的方式去判断,元素的相距距离减去容器的滚动高度,结果小于容器的 clientHeight 时那么说明元素一定在容器下边框的上方

结合两个条件可以得到一个判断方式:

1
2
3
4
5
6
7
8
9
const elOffsetTop = el.offsetTop
const containerScrollTop = container.scrollTop
const elClientHeight = el.clientHeight
const containerClientHeight = container.clientHeight
const elPosY = elOffsetTop - containerScrollTop

if (elPosY < containerClientHeigt && elPosY > -elClientHeight) {
// 处在容器的可视范围内
}

这个方式比较通用,因为这些属性都是浏览器早期就支持的,但是要注意重排的问题,这里多次访问元素的位置属性,会造成多次重排。

第二种方法是使用现成的 IntersectionObserver API,这个 API 是后续才加入浏览器中的,所以对于一些早期的浏览器可能兼容性不高,但论使用时的复杂程度,这个 API 要比前一个方法简单:

1
2
3
4
5
6
7
8
9
const io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
// 处在容器的可视范围内
}
})
})

io.observe(el)