一 如何判断一个元素是否在可视区域内?

1.1 ⽤途

可视区域即我们浏览⽹⻚的设备⾁眼可⻅的区域,如下图

在⽇常开发中,我们经常需要判断⽬标元素是否在视窗之内或者和视窗的距离⼩于⼀个值(例如 100px),从⽽实现⼀些常⽤的功能,例如:

  • 图⽚的懒加载
  • 列表的⽆限滚动
  • 计算⼴告元素的曝光情况
  • 可点击链接的预加载

1.2 实现⽅式

判断⼀个元素是否在可视区域,我们常⽤的有三种办法:

  • offsetTop、scrollTop
  • getBoundingClientRect
  • Intersection Observer

1.2.1 offsetTop、scrollTop

offsetTop ,元素的上外边框⾄包含元素的上内边框之间的像素距离,其他 offset 属性如下图所⽰:

下⾯再来了解下 clientWidth 、 clientHeight :

  • clientWidth :元素内容区宽度加上左右内边距宽度,即 clientWidth = content + padding
  • clientHeight :元素内容区⾼度加上上下内边距⾼度,即 clientHeight = content + padding

这⾥可以看到 client 元素都不包括外边距

最后,关于 scroll 系列的属性如下:

  • scrollWidth 和 scrollHeight 主要⽤于确定元素内容的实际⼤⼩
  • scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置
    • 垂直滚动 scrollTop > 0
    • ⽔平滚动 scrollLeft > 0
  • 将元素的 scrollLeft 和 scrollTop 设置为 0,可以重置元素的滚动位置

1.2.1.1 注意

  • 上述属性都是只读的,每次访问都要重新开始

下⾯再看看如何实现判断:

公式如下:

el.offsetTop - document.documentElement.scrollTop <= viewPortHeight

代码实现:

function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
 const scrollTop = document.documentElement.scrollTop
 const top = offsetTop - scrollTop
 return top <= viewPortHeight
 }

1.2.2 getBoundingClientRect

返回值是⼀个 DOMRect 对象,拥有 left,top,right,bottom,x,y,width,和 height 属性

const target = document.querySelector('.target');
 const clientRect = target.getBoundingClientRect();
 console.log(clientRect);
 // {
 // bottom: 556.21875,
 // height: 393.59375,
 // left: 333,
 // right: 1017,
 // top: 162.625,
 // width: 684
 // }

属性对应的关系图如下所⽰:

当⻚⾯发⽣滚动的时候, top 与 left 属性值都会随之改变

如果⼀个元素在视窗之内的话,那么它⼀定满⾜下⾯四个条件:

  • top ⼤于等于 0
  • left ⼤于等于 0
  • bottom ⼩于等于视窗⾼度
  • right ⼩于等于视窗宽度

实现代码如下:

function isInViewPort(element) {
 const viewWidth = window.innerWidth || document.documentElement.clientWidth;
 const viewHeight = window.innerHeight || document.documentElement.clientHeight;
 const {
 top,
 right,
 bottom,
 left,
 } = element.getBoundingClientRect();
 return (
 top >= 0 &&
 left >= 0 &&
 right <= viewWidth &&
 bottom <= viewHeight
 );
 }

1.2.3 Intersection Observer

Intersection Observer 即重叠观察者,从这个命名就可以看出它⽤于判断两个元素是否重叠,因为不⽤进⾏事件的监听,性能⽅⾯相⽐ getBoundingClientRect 会好很多

使⽤步骤主要分为两步:创建观察者和传⼊被观察者

1.2.3.1 创建观察者

const options = {
 // 表⽰重叠⾯积占被观察者的⽐例,从 0 - 1 取值,
 // 1 表⽰完全被包含
 threshold: 1.0,
 root:document.querySelector('#scrollArea') // 必须是⽬标元素的⽗级元素
 };
 const callback = (entries, observer) => { ....}
 const observer = new IntersectionObserver(callback, options);

通过 new IntersectionObserver 创建了观察者 observer ,传⼊的参数 callback 在重叠⽐例超过 threshold 时会被执⾏

关于 callback 回调函数常⽤属性如下:

// 上段代码中被省略的 callback
 const callback = function(entries, observer) {
 entries.forEach(entry => {
 entry.time; // 触发的时间
 entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
 entry.boundingClientRect; // 被观察者的位置举⾏
 entry.intersectionRect; // 重叠区域的位置矩形
 entry.intersectionRatio; // 重叠区域占被观察者⾯积的⽐例(被观察者不是矩形时也按照矩形计算)
 entry.target; // 被观察者
 });
 };

1.2.3.2 传⼊被观察者

通过 observer.observe(target) 这⼀⾏代码即可简单的注册被观察者

const target = document.querySelector('.target');
 observer.observe(target);

1.2.4 案例分析

实现:创建了⼀个⼗万个节点的⻓列表,当节点滚⼊到视窗中时,背景就会从红⾊变为⻩⾊ Html 结构如下:

<div class="container"></div>

css 样式如下:

.container {
 display: flex;
 flex-wrap: wrap;
 }
 .target {
 margin: 5px;
 width: 20px;
 height: 20px;
 background: red;
 }

往 container 插⼊1000个元素

const
 $container = $
 (".container");
 // 插⼊ 100000 个 <div class="target"></div>
 function createTargets() {
 const htmlString = new Array(100000)
 .fill('<div class="target"></div>')
 .join("");
 $container.html(htmlString);
 }

这⾥,⾸先使⽤ getBoundingClientRect ⽅法进⾏判断元素是否在可视区域

function isInViewPort(element) {
 const viewWidth = window.innerWidth || document.documentElement.clientWidth;
 const viewHeight =
 window.innerHeight || document.documentElement.clientHeight;
 const { top, right, bottom, left } = element.getBoundingClientRect();
 return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
 }

然后开始监听 scroll 事件,判断⻚⾯上哪些元素在可视区域中,如果在可视区域中则将背景颜⾊设置为 yellow

$$(window).on("scroll", () => {
 console.log("scroll !");
 $$targets.each((index, element) => {
 if (isInViewPort(element)) {
 $(element).css("background-color", "yellow");
 }
 });
 });

通过上述⽅式,可以看到可视区域颜⾊会变成⻩⾊了,但是可以明显看到有卡顿的现象,原因在于我们绑定了 scroll 事件, scroll 事件伴随了⼤量的计算,会造成资源⽅⾯的浪费

下⾯通过 Intersection Observer 的形式同样实现相同的功能

⾸先创建⼀个观察者

const observer = new IntersectionObserver(getYellow, { threshold: 1.0 });

getYellow 回调函数实现对背景颜⾊改变,如下:

function getYellow(entries, observer) {
 entries.forEach(entry => {
 $(entry.target).css("background-color", "yellow");
 });
 }

最后传⼊观察者,即 .target 元素

$targets.each((index, element) => {
 observer.observe(element);
 });

可以看到功能同样完成,并且⻚⾯不会出现卡顿的情况