90 строк кода, 15 элементов для бесконечной прокрутки

JavaScript React.js
90 строк кода, 15 элементов для бесконечной прокрутки

предисловие

В этой статье вы узнаете:

  • IntersectionObserver APIиспользование, и как быть совместимым.
  • какReact HookБесконечная прокрутка реализована в .
  • Как правильно отображать списки до 10000 элементов.

Технология бесконечного выпадающего списка позволяет пользователям пролистывать фрагменты контента. Этот подход продолжает загружать новый контент по мере прокрутки вниз.

Когда вы используете прокрутку в качестве основного метода обнаружения данных, это может удерживать ваших пользователей на странице дольше и повышать вовлеченность пользователей. С ростом популярности социальных сетей пользователи потребляют огромные объемы данных. Беспроводная прокрутка предоставляет пользователям эффективный способ просматривать большие объемы информации, не дожидаясь предварительной загрузки страницы.

Как создать отличную бесконечную прокрутку, это тема, с которой сталкивается каждый фронтенд, будь то проект или интервью.

Первоначальная реализация этой статьи исходит из:Creating Infinite Scroll with 15 Elements

1. Ранние решения

Что касается бесконечной прокрутки, ранние решения в основном полагаются на прослушивание события прокрутки:

function fetchData() {
  fetch(path).then(res => doSomeThing(res.data));
}

window.addEventListener('scroll', fetchData);

Затем рассчитать различные.scrollTop(),.offset().topи т.п.

Писать от руки тоже очень скучно. а также:

  • scrollСобытия срабатывают часто, поэтому нам также нужно регулировать вручную.
  • Внутри большое количество элементов прокруткиDOM, легко вызвать заикание.

Позже появляются перекрестные наблюдателиIntersectionObserver API , с участиемVue,ReactПосле такой управляемой данными структуры представления появилось общее решение для бесконечной прокрутки.

2. Наблюдатели за перекрестком:IntersectionObserver

const box = document.querySelector('.box');
const intersectionObserver = new IntersectionObserver((entries) => {
  entries.forEach((item) => {
    if (item.isIntersecting) {
      console.log('进入可视区域');
    }
  })
});
intersectionObserver.observe(box);

Стук по ключу:IntersectionObserver API является асинхронным, не запускается синхронно с прокруткой целевого элемента, а потребление производительности чрезвычайно низкое.

2.1 IntersectionObserverEntryобъект

Здесь я кратко представлю, что вам нужно использовать:

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 представляет значение сверху, справа, снизу и слева.
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 (
    <li className='img' ref={this.$bottomElement}>
    )
}

Как мы все знаем,React 16.xзапущен послеuseRefзаменить оригиналcreateRefдля отслеживания узлов DOM. Так что давайте начнем:

4. Принцип

Реализуйте компонент, который может отображать список из n элементов с фиксированным размером окна в 15 элементов: то есть только 15 существуют в бесконечной прокрутке n элементов в любой момент времени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, но поскольку мы исправляем 15domРендеринг элемента, который должен определить, следует ли прокручивать вверх или вниз.

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) <= maxStartIndex ? end - 5 : maxStartIndex; // 在上一轮的基础上计算头部
        setStart(newStart)
        setEnd(newEnd)
      }
      // 向上滚动,刷新数据
      if (entry.isIntersecting && entry.target.id === "top") {
        const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > 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 (
    <ul style={{position: 'relative'}}>
      {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 (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
      })}
    </ul>
  );

9. Как использовать

App.js:

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

function App() {
  return (
    <div className="App">
     <h1>15个元素实现无限滚动</h1>
      <SlidingWindowScrollHook list={MY_ENDLESS_LIST} height={195}/>
    </div>
  );
}

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) <= maxStartIndex ? end - 5 : maxStartIndex;
        setStart(newStart)
        setEnd(newEnd)
      }
      // Scroll up
      if (entry.isIntersecting && entry.target.id === "top") {
        const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > 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 (
    <ul style={{position: 'relative'}}>
      {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 (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
      })}
    </ul>
  );
}
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Версия

340 000 загрузок в неделю, не волнуйтесь, вонючие братья.

Адрес источника проекта:GitHub.com/Roger-Hi RO/…

Справочная статья:

❤️ После прочтения трех вещей

Если вы найдете этот контент вдохновляющим, я хотел бы пригласить вас сделать мне три небольших одолжения:

  1. Ставьте лайк, чтобы больше людей увидело этот контент
  2. Обратите внимание на паблик «Учитель фронтенд-убеждения», и время от времени делитесь оригинальными знаниями.
  3. Также смотрите другие статьи