IntersectionObserver 能帮你做什么?

前言

在项目开发中,往往需要去判断一个元素是否被用户看见(进入可视区),而这是为了去实现某些目的,例如:

  • 优化资源加载、渲染
    • 图片懒加载
    • 无限滚动(数据分批渲染)
    • 虚拟列表
  • 添加动画效果
    • 对于进入可视区的元素添加动画,让页面更具有 交互感
  • 判断广告曝光
    • 用于去判断页面中的广告点位是否被曝光

之前的做法就是通过监听 scroll 事件,然后再事件 回调 中调用 目标元素getBoundingClientRect() 方法,获取其相对于 视口左上角的坐标,从而判断其是否存在于可视区中。

function isElementInViewport(element) {
  const rect = element.getBoundingClientRect();
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
 
  
  const isInVerticalViewport = rect.top <= viewportHeight && rect.bottom >= 0;
 
  
  const isInHorizontalViewport = rect.left <= viewportWidth && rect.right >= 0;
 
  return isInVerticalViewport || isInHorizontalViewport;
}

window.addEventListener('scroll', () => {
    const inViewport = isElementInViewport(document.querySelector('.target-item'));
    ...
});

image.png

但上述方式存在由于 滚动事件频发 导致频繁调用元素的 getBoundingClientRect() 进而引发页面的 重排或回流,而现在通过 IntersectionObserver 可以更便捷的实现这个功能,避免造成性能问题。

IntersectionObserver

元素是否可见,实际上就是 目标元素可视区 是否产生了 交叉区,因此 IntersectionObserver 也被称为 “交叉观察器”

如下是其简单的一个使用:

const isView = ref(false);

onMounted(() => {
  const intersectionObserver = new IntersectionObserver((entries) => {
    console.log(1111);
    
    isView.value = entries[0].intersectionRatio > 0;
  },{
    root: document.querySelector('.box')!,
  });
  
  intersectionObserver.observe(document.querySelector('.item_10')!);
});

1.gif

通过这个例子我们来简单介绍下其相关的配置 new IntersectionObserver(callback[, options]),更具体可见 此处

callback 回调函数

执行时机

通过上述例子不难发现,其回调函数的执行时机 默认只有两次

  • 目标元素 进入视口
  • 目标元素 离开视口

这也是为什么前面说使用 IntersectionObserver API 比之前在 scroll 事件 回调中判断目标元素是否进入视口性能更好的原因之一。

但实际上我们还可以通过 options.threshold 配置项来设定其更具体的触发时机,后面会提到。

接收参数

var observer = new IntersectionObserver((entries, observer) => {...});

options 配置项

var observer = new IntersectionObserver(callback, {
  root: document.querySelector('.container'),
  rootMargin: '0px 0px 0px 0px',
  threshold: [0.25, 0.5, 0.75, 1]
});
  • root

    • 传入监听 目标元素的祖先元素,会将传入的元素作为 视口元素,如果没有指定默认就为 整个页面视口
  • rootMargin

    • 顾名思义,实际上就相当于 CSSmargin 属性,可以放大或缩小 视口元素 的判定范围,默认值是 "0px 0px 0px 0px"
    • 例如,0px 0px -100px 0px 相当于 视口元素 下边缘向上 收缩 100px,此时需要 目标元素顶部 进入可视区域 >= 100px 才会触发回调
  • threshold

    • 能够控制回调函数的触发时机,实际上它指定了 目标元素视口元素 之间的 交叉比例,是 0.01.0 之间的数组
    • 例如,[0.25, 0.5, 0.75, 1] 相当于指定了 目标元素视口元素 交叉比例达到 25%、50%、75%、100% 时各触发一次回调函数

    2.gif

实例方法

应用场景

常见的图片懒加载、无限滚动、虚拟列表等这里就不再实现,我们来看一些其他的场景吧。

标题与内容的联动

很多站点都会有这样的功能,以 Vue 文档 为例,如下:
3.gif

如果你有兴趣去查看一下其实现,会发现其用的就是 IntersectionObserver

image.png

具体实现

观察上述效果,不难看出就是一个双向交互:

于是可以很容易就写出如下效果:

1.gif

<template>
  
  <div class="title-box">
    <div class="active-line" :style="{ top: activeOffsetTop + 'px' }">div>
    <a
      :class="[
        'title-item',
        `title-item-${item.id}`,
        item.active ? 'active' : '',
      ]"
      :href="`#${item.id}`"
      v-for="(item, i) in data"
      :key="item.id"
      @click="clickAction($event, item.id)"
      >{{ item.title }}
  div>

  
  <div class="content-box">
    <div class="content-item" v-for="item in data" :data-id="item.id">
      <h3 :id="item.id">{{ item.title }}h3>
      <div class="content-item-text" v-for="text in item.content">
        {{ text }}
      div>
    div>
  div>
template>

<script setup lang="ts">
...
const data = ref([
  {
    id: "1",
    title: "标题 1",
    content: ["内容 1".repeat(100)],
    active: true,
  },
  ...
]);

let intersectionObserver: IntersectionObserver;

const observe = () => {
  const root = document.querySelector(".content-box")!;
  const contents: HTMLElement[] = [] = Array.from(document.querySelectorAll(".content-item")!);

  
  intersectionObserver = new IntersectionObserver(
    (entries) => {
      
      entries.forEach((entry) => {
        const { target, isIntersecting } = entry;
        
        if (isIntersecting) {
          const id = target.dataset.id;
          clickAction(
            { target: document.querySelector(`.title-item-${id}`), mock: true },
            id
          );
        }
      });
    },
    {
      root,
      threshold: [0.5],
    }
  );

  
  contents.forEach((target) => {
    intersectionObserver.observe(target);
  });
};

onMounted(() => {
  observe();
});

onBeforeUnmount(() => {
  intersectionObserver.disconnect();
});


const activeOffsetTop = ref(0);


const clickAction = (e, id: string) => {
  data.value.forEach((v) => {
    if (id === v.id) {
      v.active = true;
      activeOffsetTop.value = e.target.offsetTop;
    } else {
      v.active = false;
    }
  });
};
script>

存在缺点

基于 IntersectionObserver 实现很便捷,但也会存在一个缺点,如下所示:

2.gif

当点击 还有其他问题? 标题时,选中样式不会在此标题上,而是选中了它下面的那个标题 选择你的学习路径,这是为啥呢?

85B64B42.gif

这是因为 还有其他问题? 标题对应的内容很短,于是在基于 a 标签 锚点跳转的时候,将它下面标题的内容也带入了 可视区 中,此时 IntersectionObserver 的回调就会触发,所以最终选中的标题就是 选择你的学习路径

入场动画

IntersectionObserver API 的特性非常适合用来处理元素的 入场动画,很多官网的动画也是这么去实现的,那下面就实现一个简单的入场动画,如下:

3.gif

Element.animate()

这里使用 Element.animate() 方法来执行上面这个简单的动画,针对一些较为复杂的动画可以通过 @keyframes + animation 的方式定义在具体的 选择器 中,然后在目标元素进入视口时为其添加对应的 选择器 即可。

Element.animate() 方法接收两个参数:

  • keyframes,关键帧对象数组或一个关键帧对象,关键帧格式
  • options,代表动画持续时间的整数(以毫秒为单位),或者一个包含一个或多个时间属性,KeyframeEffect() 参数

看着很抽象,实际上就是把之前在 CSS 写动画的方式抽离到 JS 中了。

具体实现



<template>
  <div class="list">
    <div class="item" v-for="i in 20" :data-index="i">{{ i }}div>
  div>
template>

兼容性

从下图可以看出各主流浏览器都有相应的实现,针对一些兼容性比较差的可以使用对应的 polyfill

image.png

最后

IntersectionObserver API 提供给我们一种更便捷的判断目标元素是否处于可视区的方式,它相对于传统滚动事件的实现方式来讲 性能更好,除此之外还允许我们自定义 thresholds 阈值 来实现 自定义回调触发时机,还可以实现 同时监听多个目标元素

除此之外,也要注意在 快速滚动 的场景下 IntersectionObserver 可能会不执行的问题,详情可见

0DDBDEC2.gif

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=20185,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?