如何判断图片是否在可视区域
# 前言
在性能优化方面,实现图片懒加载的功能是一项必备的技能。在实现过程时,我们需要判断图片是否在可视区域。由于页面绘制的复杂性性,需要 Check if an element is visible in a scrollable container,而网上搜出的99.99%的答案都是 Check if an element is visible within window's viewport,我也去搜索了stackoverflow 里头的答案,高赞的解决方案很多,但是把这些函数拷贝到自己的 Demo 中却是始终不生效,于是就有了这篇文章:
首先约定本文涉及到的变量含义:
- 可滚动的容器:
container- 元素:
element
# 方案一:[github]HTML DOM: offset+scrollTop
在搜索的过程中发现一个宝藏网站:https://htmldom.dev/ (opens new window)
这个网站里头罗列了几十种常见的DOM操作,没事可以多翻一翻学习,有机会一定要翻译成中文,放在github的网站里头。
滚动的三种状态:
The following functions return true if the ele element is visible in its scrollable container:
const isVisible = function (ele, container) {
const eleTop = ele.offsetTop;
const eleBottom = eleTop + ele.clientHeight;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
return (
// element 完全在container
(eleTop >= containerTop && eleBottom <= containerBottom) ||
// element 刚进入container
(eleTop < containerTop && containerTop < eleBottom) ||
// element 即将离开container
(eleTop < containerBottom && containerBottom < eleBottom)
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用方法:
var container = document.querySelector('#container');
var ele = document.querySelector('#ele');
if (isVisible(ele,container)) {
// Do something...
}
2
3
4
5
# 方案二:[github]HTML DOM: ele.getBoundingClientRect()
还有一个函数推荐的比较多element.getBoundingClientRect(),与上面的offset定位的方式不同的是,使用时不需要对postion的定位方式做要求,根据实验可得:
element.getBoundingClientRect():元素相对于windows的Viewport的距离。client.getBoundingClientRect():包在元素外层的container到Viewport的距离。
因此,一般来说,
client要小于element的距离。
注解:虽然网上推荐此函数比较多,但是大部分的场景还是在判断
element是否在哎window的viewport可视区域,而如果要用于识别是否在container可视区域,就需要将两者相减,即将两个scrollTop相减后与element本身的height相比。如果需要画图说明的话,还是使用
relative比较清楚。
但是这里需要注意一下,bottom和right的的参考和传统的定位有所区别,始终以上边框和左边框作为基准线:
const { bottom, height, top1 } = ele.getBoundingClientRect();
const { top2, bottom2} = container.getBoundingClientRect();
// 这里用一个三元表达式,将上面的滚动的三种方式已经囊括在里头了。
return top1 <= top2 ? top2 - top1 <= height : bottom1 - bottom2 <= height;
2
3
4
# 方案三:IntersectionObserver API
上面两种方案都使用到了**window.scroll 监听 Element.getBoundingClientRect() 并使用 _.throttle 节流**,浏览器其实已经内置了一个监听器完成这些操作了,可以通过读取changes[item]的intersetionRatio和isIntersecting属性判断是否元素重叠。
this.observer = new IntersectionObserver((changes) => {
// changes: 目标元素集合(需要取一下才可以)
console.log('intersectionRatio', changes[0].intersectionRatio);
console.log('isIntersecting', changes[0].isIntersecting);
});
2
3
4
5
# 🚨 注意:position的设置
在调试以上代码的时候,调试至少花费了有4个小时,总是被几个非常基础的问题给卡住,主要开在的是css属性的设置上,尤其是offset的取值上,由于没有考虑到position的设置,offset总是计算element 到 windows 视图窗的高度。
将position设置为relative的前提条件,也是在stackoverflow (opens new window)另一个问题的解答中提到的,如果它不提估计我又要卡好久,感谢感谢,stackoverflow真yyds!

# 兼容性说明:
不同浏览器在获取window的可视高度方法是不同的,考虑到兼容性,常可以看到以下写法:
let vieportWidth = (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
let vieportHeight = (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight)
2
还有一个例子,就是当需要获取页面的滚动高度scrollTop时,经常写成下面这段代码:
var scrollTop = window.pageYOffset // 兼容 FireFox
|| document.documentElement.scrollTop
|| document.body.scrollTop
|| 0;
2
3
4
在 IE 和 Firefox 中:
- 当页面指定
DOCTYPE(DTD)时,使用document.documentElement。- 当页面没有指定
DOCTYPE(DTD)时,使用document.body。
# 总结
scrollHeight:只有container才有scroll相关属性,选中element无效果。scrollTop:一般情况下为0,只有滚动起来才可会有值,【滚动时:不变】
offsetTop:其值取决于container上设置的position值。如果
position:static,返回的是element到windows的距离如果
position:relative,返回的是element到container的距离。
# 使用 lodash 进行优化
import _ from "lodash"
class Example extends Component {
constructor(props) {
super(props);
// 加入 防抖 操作
this.isVisible3 = _.throttle(this.isVisible2,1000)
}
isVisible = (container, ele) => {
const eleTop = ele.offsetTop;
const eleBottom = eleTop + ele.clientHeight;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
return (
(eleTop >= containerTop && eleBottom <= containerBottom) ||
(eleTop < containerTop && containerTop < eleBottom) ||
(eleTop < containerBottom && containerBottom < eleBottom)
);
};
isVisible2 = (container, ele) => {
const { bottom, height, top } = ele.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return top <= containerRect.top ? containerRect.top - top <= height : bottom - containerRect.bottom <= height;
};
componentDidMount() {
const container = document.querySelector(".container")
const ele = document.getElementById("ele")
oContainer.addEventListener('scroll', () => {
console.log(this.isVisible3(oContainer, ele))
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32