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

251 阅读5分钟

面试官:怎么判断元素是否在可视区域内?
我:使用offsetTop - scrollTop,如果得到的值小于等于clientHeight,就是在可视区域内
面试官:还有其他方法吗?
我:还可以通过getBoundingClientRect方法获取元素的位置进行判断
面试官:还有吗?
我:暂时只了解到这两种
面试官:回去再好好了解其他方法...

不管是图片懒加载、无限滚动列表、VirtualScroll 虚拟滚动列表、滚动条滚动到指定元素触发动画效果等,都涉及到怎么判断元素是否在可视区域内这个问题。那么到底有几种方法?这些方法对比有哪些区别?

1. 元素距离页面顶部高度 offsetTop - 滚动条滚动高度 scrollTop <= 屏幕可视窗口高度 screenHeight

const dom = document.getElementById("");
window.onscroll = () => {
  // 获取可视窗口的高度。
  const clientHeight = document.body.clientHeight;
  // 获取滚动条滚动的高度
  const scrollTop = document.documentElement.scrollTop;
  // 获取元素偏移的高度。就是距离可视窗口的偏移量。
  const offsetTop = dom.offsetTop;
  if (offsetTop - scrollTop <= clientHeight) {
    // 在可视区域内...
  } else {
    // 在可视区域外...
  }
};

2. getBoundingClientRect

getBoundingClientRect 是 dom 对象的一个方法,该方法返回元素大小和它相对于视口的位置属性,位置属性分别为:

  • top:元素上边框到视口顶端距离。
  • left:元素左边框到视口左端距离。
  • bottom:元素下边框到视口顶端距离。
  • right:元素右边框到视口左端距离。
  • width:元素的宽度(可选)。
  • height:元素的高度(可选)。 那怎么判断子元素是否在可视区域内?答案是:top 大于等于 0 && left 大于等于 0 && bottom 小于等于屏幕可视窗口高度 && right 小于等于屏幕可视窗口高度
const dom = document.getElementById("");
window.onscroll = () => {
  const clientHeight = document.body.clientHeight;
  const clientWidth = document.body.clientWidth;
  // 当滚动条滚动时,位置信息发生改变
  const { top, right, bottom, left } = dom.getBoundingClientRect();
  if (top >= 0 && left >= 0 && right <= clientWidth && bottom <= clientHeight) {
    // 在可视区域内...
  } else {
    // 在可视区域外...
  }
};

3. IntersectionObserver(交叉观察器)

IntersectionObserver 是一个用于观察目标元素与 root 根元素之间交叉状态变化的 API,并且是异步的,不随着目标元素的滚动同步触发。 它接收 2 个参数,第 1 个参数是 callback 回调函数,第 2 个参数是 options 配置对象。

3.1 callback 回调函数触发时机
  • Observer 第一次监听目标元素的时候,也就是初始化时,触发回调函数
  • 当目标元素进入或退出 root 根元素时,或者两个元素的相交部分大小发生变化时,触发回调函数
3.1.1 callback 回调函数接收两个参数:entries(返回目标元素的交叉信息)和 observer(观察者实例)

主要讲解一下 entries:它是一个数组,数组中包含了多个 IntersectionObserverEntry 实例,每个实例代表着一个目标元素与 root 根元素相交的信息。

  • intersectionRatio 返回目标元素和 root 根元素相交区域占目标元素的比例值,范围是 0~1
  • intersectionRect 返回一个 DOMRectReadOnly 对象,描述目标元素和 root 根元素相交区域的边界信息
  • isIntersecting 返回目标元素和 root 根元素是否相交的布尔值
  • rootBounds 返回一个 DOMRectReadOnly 对象,描述 root 根元素的边界信息
  • target 目标元素,也就是当前 IntersectionObserverEntry 实例所对应的 DOM 元素
  • time 相交时间的时间戳
3.2 options 配置对象
  • root 指定根元素,以 dom 对象方式接收.它必须是目标元素的父级元素。如果未指定或者为 null,则默认为浏览器视窗。
  • rootMargin 相当于 root 元素多了一个 margin 属性,如果没有这个 margin 属性,目标元素只有与 root 元素开始交叉时触发。而设置了 rootMargin 后,目标元素与 root 元素的外边距交叉时就会触发。默认值为四个边距全是 0。
  • threshold root 根元素和目标元素相交程度达到 threshold 设置的值,回调函数被调用。threshold 可以是单一的 number 也可以是 number 数组。例如:设置为 0.5 时,root 根元素和目标元素相交程度达到 50%就触发回调函数; 设置为[0.25, 0.5]时,root 根元素和目标元素相交程度分别达到 25%和 50%时都会触发回调函数。

下面是我画的相交示例图(画的很丑,莫见怪...)

IOflow.png

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IntersectionObserver 示例</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .content {
        height: 2000px;
        background-color: aquamarine;
      }

      #show-content {
        height: 200px;
        background: orange;
        text-align: center;
      }

      #info-box {
        width: 200px;
        height: 200px;
        line-height: 200px;
        position: fixed;
        top: 40%;
        right: 50px;
        background-color: bisque;
        text-align: center;
      }
    </style>
  </head>

  <body>
    <div class="content"></div>
    <div id="show-content">我出现啦</div>
    <div class="content"></div>
    <div id="info-box">相交了</div>
    <script>
      const options = {
        root: null, // 设置为null或者未指定,默认为浏览器视窗
        rootMargin: "0px",
        threshold: 0.25, // 相交25%时会触发回调
      };
      const io = new IntersectionObserver((entries) => {
        console.log("entries", entries);
        // isIntersecting 如果root根元素与目标元素相交并且达到threshold设置的相交程度,则为true,反之为false
        if (entries[0].isIntersecting) {
          document.getElementById("info-box").innerHTML = "相交了";
        } else {
          document.getElementById("info-box").innerHTML = "离开了";
        }
      }, options);
      const dom = document.getElementById("show-content");
      io.observe(dom);
    </script>
  </body>
</html>

总结一下

1(dom.offsetTop和滚动条高度差 <= 视窗高度) 和 2(getBoundingClientRect) 都是需要监听滚动条滚动事件,并且需要频繁调用元素的位置方法来获取元素的边界信息。事件监听和调用元素的位置方法都是在主线程上运行的,所以频繁触发、调用可能会造成性能问题。如果页面有很多滚动时的动画效果,性能消耗更大,页面卡顿问题就比较明显。
所以在 2016 年初,chrome51 率先推出了 IntersectionObserver 这个 API 来解决以上问题。它是一个异步的实例,只有在浏览器空闲的状态下才会触发,如果浏览器当前的事件队列中,有一系列的回调函数正在等待处理,该方法是不会被执行到的,只有当浏览器的事件队列为空,浏览器在空闲的时候,才会执行该方法。但这个API到现在为止一些旧版本的浏览器兼容性存在问题,没有前两种兼容性那么好。