react 使用 IntersectionObserver API 实现自动滚动

网页开发时,常常需要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它。

上图的绿色方块不断滚动,顶部会提示它的可见性。

传统的实现方法是,监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题

目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"。

IntersectionObserver 特点

  • 非阻塞性质:该 API 以异步方式运行,利用浏览器的高效内部处理机制,不会影响到主线程的运行,有效避免了性能瓶颈。
  • 资源高效:与传统的滚动事件监听或定时器相比,IntersectionObserver 能够精确监测元素与视窗的相交情况,减少了不必要的计算和回调函数的触发,从而降低了资源消耗。
  • 多元素监控:IntersectionObserver 允许同时对多个元素进行监控,通过回调函数逐一通知开发者在视窗中的相交状态,便于实现批量处理。
  • 阈值自定义:开发者可以设置一个或多个阈值,以定义元素与视窗相交的程度。一旦相交比例达到或低于这些阈值,便会激活相应的回调函数。

利用 IntersectionObserver,开发者能够轻松实现诸如图片懒加载、滚动加载更多内容、广告可见性控制等功能,这些都能够显著提高网页的性能和用户的互动体验

一、API

语法:


var io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。


// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

io.observe(elementA);
io.observe(elementB);

二、callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback

callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。


var io = new IntersectionObserver(
  entries => {
    console.log(entries);
  }
);

上面代码中,回调函数采用的是箭头函数的写法。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

三、IntersectionObserverEntry 对象

IntersectionObserverEntry对象提供目标元素的信息,一共有六个属性。


{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}

每个属性的含义如下:

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。它们各自的intersectionRatio图中都已经注明。

演示示例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <style>
    div {
      height: 500px;
      width: 30%;
      margin-bottom: 50px;
      }
      
      #a {
      background-color: red;
        float: left;
      }
      
      #b {
      background-color: black;
        float: left;
      }
      
      #c {
      background-color: blue;
      clear: left;
      }
  </style>
</head>
<body>
  <div id="a"></div>
  <div id="b"></div>
  <div id="c"></div>
</body>
<script>
  var io = new IntersectionObserver(
    entries => {
      entries.forEach(i => {
        console.log('Time: ' + i.time);
        console.log('Target: ' + i.target);
        console.log('IntersectionRatio: ' + i.intersectionRatio);
        console.log('rootBounds: ' + i.rootBounds);
        console.log(i.boundingClientRect);
        console.log(i.intersectionRect);
        console.log('================');
      });
    },
    {
        /* Using default options. Details below */
    }
);
// Start observing an element
io.observe(document.querySelector('#a'));
io.observe(document.querySelector('#b'));
</script>
</html>

注意,这个 Demo 只能在 Chrome 51+ 运行。

四、Option 对象

IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性。

threshold 属性

  • threshold属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。

new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0, 0.25, 0.5, 0.75, 1]
  }
);

用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

root 属性,rootMargin 属性

  • 很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe窗口里滚动)。容器内滚动也会影响目标元素的可见性,参见本文开始时的那张示意图。
  • IntersectionObserver API 支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。

var opts = { 
  root: document.querySelector('.container'),
  rootMargin: "500px 0px" 
};

var observer = new IntersectionObserver(
  callback,
  opts
);

上面代码中,除了root属性,还有rootMargin属性。后者定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。

这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。

五、注意点

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。

规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

六、参考来源

阮一峰:IntersectionObserver API 使用教程 - 阮一峰的网络日志

案例--使用 IntersectionObserver 实现自动无限滚动

效果

实现代码

import React, { useEffect, useRef, useState } from "react";
import styles from "./index.module.scss";

const dataSource = new Array(50).fill(0).map((_, index) => index + 1);
const ITEM_5_ID = "item-5";

export default function CycleScrollList() {
  const [data, setData] = useState(dataSource.slice(0, 10));

  const intersectionObserverRef = useRef();
  const item5Ref = useRef(null);

  const nextIndex = useRef(10); // 持续从 dataSource 拿数据的下一个 index
  const justVisible5 = useRef(false); // 原来是否为可视

  useEffect(() => {
    intersectionObserverRef.current = new IntersectionObserver((entries) => {
      entries.forEach((item) => {
        if (item.target.id === ITEM_5_ID) {
          // 与视图相交(开始出现)
          if (item.isIntersecting) {
            justVisible5.current = true;
          }
          // 从可视变为不可视
          else if (justVisible5.current) {
            replaceData();
            justVisible5.current = false;
          }
        }
      });
    });
    startObserver();

    return () => {
      intersectionObserverRef.current?.disconnect();
      intersectionObserverRef.current = null;
    };
  }, []);

  const startObserver = () => {
    if (item5Ref.current) {
      // 对第五个 item 进行监测
      intersectionObserverRef.current?.observe(item5Ref.current);
    }
  };

  const replaceData = () => {
    let newData = [];
    if (nextIndex.current - 5 < 0) {
      newData = [
        ...dataSource.slice(nextIndex.current - 5),
        ...dataSource.slice(0, nextIndex.current + 5),
      ];
    } else {
      newData = [
        ...dataSource.slice(nextIndex.current - 5, nextIndex.current + 5),
      ];
    }
    // 使用当前的后半份数据,再从 dataSource 中拿新数据
    console.log(newData);
    const nextIndexTemp = nextIndex.current + 5;
    const diff = nextIndexTemp - dataSource.length;
    if (diff < 0) {
      nextIndex.current = nextIndexTemp;
    } else {
      // 一轮数据用完,从头继续
      nextIndex.current = diff;
    }
    setData(newData);
  };

  return (
    <div className={styles.box}>
      <div className={styles.visibleView}>
        <div className={styles.container}>
          {data.map((item, index) =>
            index === 4 ? (
              <div
                id={ITEM_5_ID}
                ref={item5Ref}
                key={index}
                className={styles.div}
              >
                {item}
              </div>
            ) : (
              <div key={index} className={styles.div}>
                {item}
              </div>
            )
          )}
        </div>
      </div>
    </div>
  );
}

index.module.scss

$itemHeight: 60px; // 单个item的高度

$itemShowTime: 3s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 5; // 单个循环上移的item条数
$oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-$itemHeight * $oneCycleItemNum);
  }
}
.container {
  height: 600px;

  animation: dynamics-rolling $oneCycleItemTime linear infinite;
  .div {
    line-height: 60px;
  }
}

.visibleView {
  width: 100%;
  height: 120px;
  overflow: hidden;
  background-color: skyblue;

}
.box {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

React-virtualized是一个非常流行的React库,它可以帮助我们实现大数据量的虚拟滚动效果。下面是一个使用React-virtualized实现虚拟滚动的表格的示例代码: 首先,我们需要安装React-virtualized库: ``` npm install react-virtualized --save ``` 然后,我们需要引入Table和Column组件: ``` import { Table, Column } from 'react-virtualized'; ``` 接下来,我们可以定义一个数据源,例如: ``` const list = [ { name: '张三', age: '18', address: '北京市海淀区' }, { name: '李四', age: '20', address: '北京市朝阳区' }, { name: '王五', age: '22', address: '北京市西城区' }, // ... // 这里可以添加更多的数据 ]; ``` 然后,我们可以定义一个Table组件,指定它的rowCount和rowGetter属性: ``` <Table rowCount={list.length} rowGetter={({ index }) => list[index]} > ``` 接下来,我们可以添加一些Column组件,定义每一列的属性: ``` <Column label="姓名" dataKey="name" width={100} /> <Column label="年龄" dataKey="age" width={100} /> <Column label="地址" dataKey="address" width={200} /> ``` 最后,我们需要在Table组件中添加一些属性,以启用虚拟滚动: ``` <Table rowCount={list.length} rowGetter={({ index }) => list[index]} headerHeight={20} rowHeight={30} width={600} height={400} > ``` 在上面的代码中,我们设置了headerHeight和rowHeight属性来指定表头和每一行的高度,width和height属性用于指定表格的宽度和高度。React-virtualized自动根据这些属性来计算出需要渲染的行数,并且只渲染当前可见的行,以实现虚拟滚动的效果。 完整的代码示例: ``` import React, { Component } from 'react'; import { Table, Column } from 'react-virtualized'; const list = [ { name: '张三', age: '18', address: '北京市海淀区' }, { name: '李四', age: '20', address: '北京市朝阳区' }, { name: '王五', age: '22', address: '北京市西城区' }, // ... // 这里可以添加更多的数据 ]; class VirtualTable extends Component { render() { return ( <Table rowCount={list.length} rowGetter={({ index }) => list[index]} headerHeight={20} rowHeight={30} width={600} height={400} > <Column label="姓名" dataKey="name" width={100} /> <Column label="年龄" dataKey="age" width={100} /> <Column label="地址" dataKey="address" width={200} /> </Table> ); } } export default VirtualTable; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白小白从不日白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值