90行代码,15个元素实现无限滚动

IntersectionObserverEntry 对象

callback 函数被调用时,会传给它一个数组,这个数组里的每个对象就是当前进入可视区域或者离开可视区域的对象( IntersectionObserverEntry 对象)

这个对象有很多属性,其中最常用的属性是:

  • target : 被观察的目标元素,是一个 DOM 节点对象

  • isIntersecting : 是否进入可视区域

  • intersectionRatio : 相交区域和目标元素的比例值,进入可视区域,值大于0,否则等于0

2.3 options

调用 IntersectionObserver 时,除了传一个回调函数,还可以传入一个 option 对象,配置如下属性:

  • threshold : 决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

  • root : 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素

  • rootMargin : 用来扩大或者缩小视窗的的大小,使用css的定义方法,10px 10px 30px 20px表示top、right、bottom 和 left的值

const io = new IntersectionObserver((entries) => {
  console.log(entries);
}, {
  threshold: [0, 0.5],
  root: document.querySelector('.container'),
  rootMargin: "10px 10px 30px 20px",
});

2.4 observer

observer.observer(nodeone); //仅观察nodeOne 
observer.observer(nodeTwo); //观察nodeOne和nodeTwo 
observer.unobserve(nodeOne); //停止观察nodeOne
observer.disconnect(); //没有观察任何节点

3. 如何在React Hook中使用IntersectionObserver

在看 Hooks 版之前,来看正常组件版的:

class SlidingWindowScroll extends React.Component {
this.$bottomElement = React.createRef();
...
componentDidMount() {
    this.intiateScrollObserver();
}
intiateScrollObserver = () => {
    const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1
    };
    this.observer = new IntersectionObserver(this.callback, options);
    this.observer.observe(this.$bottomElement.current);
}
render() {
    return (
    
  • ) }
  • 众所周知, React 16.x 后推出了 useRef 来替代原有的 createRef ,用于追踪DOM节点。那让我们开始吧:

    4. 原理

    实现一个组件,可以显示具有15个元素的固定窗口大小的n个项目的列表:

    即在任何时候,无限滚动n元素上也仅存在15个 DOM 节点。

    • 采用 relative/absolute 定位来确定滚动位置

    • 追踪两个 ref : top/bottom 来决定向上/向下滚动的渲染与否

    • 切割数据列表,保留最多15个DOM元素。

    5. useState 声明状态变量

    我们开始编写组件 SlidingWindowScrollHook :

    const THRESHOLD = 15;
    const SlidingWindowScrollHook = (props) =>  {
      const [start, setStart] = useState(0);
      const [end, setEnd] = useState(THRESHOLD);
      const [observer, setObserver] = useState(null);
      // 其它代码...
    }
    

    1. useState 的简单理解:

    const [属性, 操作属性的方法] = useState(默认值);
    

    2. 变量解析

    • start :当前渲染的列表第一个数据,默认为0

    • end : 当前渲染的列表最后一个数据,默认为15

    • observer : 当前观察的视图 ref 元素

    6. useRef 定义追踪的DOM 元素

    const $bottomElement = useRef();
    const $topElement = useRef();
    

    正常的无限向下滚动只需关注一个dom元素,但由于我们是固定15个 dom 元素渲染,需要判断向上或向下滚动。

    7. 内部操作方法和和对应useEffect

    请配合注释食用:

    useEffect(() => {
        // 定义观察
        intiateScrollObserver();
        return () => {
          // 放弃观察
          resetObservation()
      }
    },[end]) //因为[end] 是同步刷新,这里用一个就行了。
    
    // 定义观察
    const intiateScrollObserver = () => {
        const options = {
          root: null,
          rootMargin: '0px',
          threshold: 0.1
        };
        const Observer = new IntersectionObserver(callback, options)
        // 分别观察开头和结尾的元素
        if ($topElement.current) {
          Observer.observe($topElement.current);
        }
        if ($bottomElement.current) {
          Observer.observe($bottomElement.current);
        }
        // 设初始值
        setObserver(Observer)    
    }
    
    // 交叉观察的具体回调,观察每个节点,并对实时头尾元素索引处理
    const callback = (entries, observer) => {
        entries.forEach((entry, index) => {
          const listLength = props.list.length;
          // 向下滚动,刷新数据
          if (entry.isIntersecting && entry.target.id === "bottom") {
            const maxStartIndex = listLength - 1 - THRESHOLD;     // 当前头部的索引
            const maxEndIndex = listLength - 1;                   // 当前尾部的索引
            const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex; // 下一轮增加尾部
            const newStart = (end - 5)  THRESHOLD ? end - 10 : THRESHOLD); // 向上滚动尾部元素索引不得小于15
            let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0); // 头部元素索引最小值为0
            setStart(newStart)
            setEnd(newEnd)
            }
        });
    }
    
    // 停止滚动时放弃观察
    const resetObservation = () => {
        observer && observer.unobserve($bottomElement.current); 
        observer && observer.unobserve($topElement.current);
    }
    
    // 渲染时,头尾ref处理
    const getReference = (index, isLastIndex) => {
        if (index === 0)
          return $topElement;
        if (isLastIndex) 
          return $bottomElement;
        return null;
    }
    

    8. 渲染界面

      const {list, height} = props; // 数据,节点高度
      const updatedList = list.slice(start, end); // 数据切割
    
      const lastIndex = updatedList.length - 1;
      return (
        
      {updatedList.map((item, index) => { const top = (height * (index + start)) + 'px'; // 基于相对 & 绝对定位 计算 const refVal = getReference(index, index === lastIndex); // map循环中赋予头尾ref const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : ''); // 绑ID return (
    • {item.value}
    • ); })}
    );

    9. 如何使用

    App.js :

    import React from 'react';
    import './App.css';
    import { SlidingWindowScrollHook } from "./SlidingWindowScrollHook";
    import MY_ENDLESS_LIST from './Constants';
    
    function App() {
      return (
        

    15个元素实现无限滚动

    ); } export default App;

    定义一下数据 Constants.js :

    const MY_ENDLESS_LIST = [
      {
        key: 1,
        value: 'A'
      },
      {
        key: 2,
        value: 'B'
      },
      {
        key: 3,
        value: 'C'
      },
      // 中间就不贴了...
      {
        key: 45,
        value: 'AS'
      }
    ]
    

    SlidingWindowScrollHook.js :

    import React, { useState, useEffect, useRef } from "react";
    const THRESHOLD = 15;
    
    const SlidingWindowScrollHook = (props) =>  {
      const [start, setStart] = useState(0);
      const [end, setEnd] = useState(THRESHOLD);
      const [observer, setObserver] = useState(null);
      const $bottomElement = useRef();
      const $topElement = useRef();
    
      useEffect(() => {
        intiateScrollObserver();
        return () => {
          resetObservation()
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
      },[start, end])
    
      const intiateScrollObserver = () => {
        const options = {
          root: null,
          rootMargin: '0px',
          threshold: 0.1
        };
        const Observer = new IntersectionObserver(callback, options)
        if ($topElement.current) {
          Observer.observe($topElement.current);
        }
        if ($bottomElement.current) {
          Observer.observe($bottomElement.current);
        }
        setObserver(Observer)    
      }
    
      const callback = (entries, observer) => {
        entries.forEach((entry, index) => {
          const listLength = props.list.length;
          // Scroll Down
          if (entry.isIntersecting && entry.target.id === "bottom") {
            const maxStartIndex = listLength - 1 - THRESHOLD;     // Maximum index value `start` can take
            const maxEndIndex = listLength - 1;                   // Maximum index value `end` can take
            const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex;
            const newStart = (end - 5)  THRESHOLD ? end - 10 : THRESHOLD);
            let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0);
            setStart(newStart)
            setEnd(newEnd)
          }
    
        });
      }
      const resetObservation = () => {
        observer && observer.unobserve($bottomElement.current);
        observer && observer.unobserve($topElement.current);
      }
    
    
      const getReference = (index, isLastIndex) => {
        if (index === 0)
          return $topElement;
        if (isLastIndex) 
          return $bottomElement;
        return null;
      }
    
      const {list, height} = props;
      const updatedList = list.slice(start, end);
      const lastIndex = updatedList.length - 1;
    
      return (
        
      {updatedList.map((item, index) => { const top = (height * (index + start)) + 'px'; const refVal = getReference(index, index === lastIndex); const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : ''); return (
    • {item.value}
    • ); })}
    ); } export { SlidingWindowScrollHook };

    以及少许样式:

    .li-card {
      display: flex;
      justify-content: center;
      list-style: none;
      box-shadow: 2px 2px 9px 0px #bbb;
      padding: 70px 0;
      margin-bottom: 20px;
      border-radius: 10px;
      position: absolute;
      width: 80%;
    }
    

    然后你就可以慢慢耍了。。。

    10. 兼容性处理

    IntersectionObserver 不兼容 Safari ?

    莫慌,我们有 polyfill

    每周34万下载量呢,放心用吧臭弟弟们。

    项目源地址:https://github.com/roger-hiro/SlidingWindowScrollHook

    参考文章:

    • Creating Infinite Scroll with 15 Elements

    • IntersectionObserve初试

    :heart: 看完三件事

    如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

    1. 点「在看」 ,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

    2. 关注公众号「前端劝退师」,不定期分享原创知识。

    3. 也看看其它文章