判断元素是否在视窗之内

2,648 阅读3分钟

作为一名前端工程师我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:

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

说明:可点击链接的预加载 这个功能目前使用的网站还比较少,其实就是“预先获取页面可视区域内的链接,加快后续加载速度”,能极大提升用户在站内跳转时的体验,由 Google 在 2018 年年底通过 quicklink 项目 进行开源。

目前流行的方式是通过 Element.getBoundingClientRect() 拿到元素的相关位置信息后进行手动的判断,但是这种方法由于运行在 JavaScript 的主进程上,所以当需要监听的元素较多时,可能会造成性能问题。

那么仔细想一想,其实在浏览器渲染的时候,它就知道了元素是否在视窗之内,自身面积有多少在视窗之内。所以最高效的方式就是预先告诉浏览器当目标元素和视窗重叠的时候,我们要搞事情,然后等着浏览器执行回调函数即可。出于这种考虑,W3C 提出了 Intersection Observer API

我做了一个小实验,创建了一个十万个节点的长列表,当节点滚入到视窗中时,背景就会从红色变为黄色。

下图是使用 Element.getBoundingClientRect() 进行计算实现的效果,可以看到有非常明显的卡顿,主要是因为需要对每一个元素都进行计算,判断它们是否在视窗之内。具体的代码可以点击查看 Code Pen

Element.getBoundingClientRect() 实现

下图是使用 Intersection Observer API 进行注册回调实现的效果,可以看出来十分流畅。具体的代码可以点击查看 Code Pen

Intersection Observer API

本文接下来就分别介绍这两种方法。

Element.getBoundingClientRect() - 手动计算

通过 Element.getBoundingClientRect(),我们可以拿到元素在视窗内的位置,包括其距离视窗的上下左右的距离和它自身的宽高。

const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();

// log data
console.log(clientRect);

// {
//   bottom: 556.21875,
//   height: 393.59375,
//   left: 333,
//   right: 1017,
//   top: 162.625,
//   width: 684
// }

可以通过来自 MDN 的一张图进行说明:

MDN - getBoundingClientRect()

如果一个元素在视窗之内的话,那么它一定满足下面四个条件:

  • 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
  );
}

// usage
console.log(isInViewPort(document.querySelector('.target'))); // true or false

Intersection Observer API 注册回调

Intersection Observer 即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠。

这个 API 使用十分简单,只需两步:创建观察者传入被观察者

创建观察者

const options = {
  // 表示重叠面积占被观察者的比例,从 0 - 1 取值,
  // 1 表示完全被包含
  threshold: 1.0, 
};

const callback = (entries, observer) => { ....}

const observer = new IntersectionObserver(callback, options);

通过上面的几行代码就创建了观察者 observer,传入的参数 callback 在重叠比例超过 threshold 时会被执行。

说明:options 支持传入更多的参数来指定根元素,未传入时使用视窗元素。

传入被观察者

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

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

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

注意:目前在浏览器的原生支持还不是很好,可以使用 w3c - IntersectionObserver Polifill 进行兼容。

欢迎大家访问我的博客 - 烂笔头