предисловие
В этой статье вы узнаете:
-
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/…
Справочная статья:
❤️ После прочтения трех вещей
Если вы найдете этот контент вдохновляющим, я хотел бы пригласить вас сделать мне три небольших одолжения:
- Ставьте лайк, чтобы больше людей увидело этот контент
- Обратите внимание на паблик «Учитель фронтенд-убеждения», и время от времени делитесь оригинальными знаниями.
- Также смотрите другие статьи
- 120 строк кода для реализации полного интерактивного компонента загрузки с помощью перетаскивания.
- 160 строк кода для создания динамичных и крутых визуальных диаграмм - Таблица лидеров
- «Король библиотеки визуализации данных» D3.js Быстрое начало работы с приложением Vue
- Руководство "True® Full Stack Road" для веб-интерфейсной разработки
- «Практика Vue» — плагин Vue CLI за 5 минут
- Вооружите свой интерфейсный проект «Практикой Vue»
- «Intermediate and Advanced Front-End Interview» JavaScript Рукописный код Invincible Cheats
- «Узнайте из исходного кода» ответы на вопросы Vue, которые интервьюеры не знают
- JS-операция «Узнать из исходного кода» в исходном коде Vue
- Правильная позиция для обновления vue-cli3 в проекте "Vue Practice"
- Почему вы до сих пор не можете понять цепочку областей видимости JavaScript?