предисловие
В проекте есть две страницы с большим количеством картинок, нарисованных Echarts.При входе он сильно застревает.После использования прокрутки загрузка стала намного плавнее,и пользовательский опыт значительно улучшился.
Кроме того, существует множество других применений загрузки прокрутки, таких как: прокрутка страниц, беспроводная прокрутка и запросы на отображение изображений в области просмотра. . .
Прикрепите ссылку на гитхаб
содержание этой статьи
- Традиционное решение реализует прокатную загрузку
- H5 API IntersectionObserver реализует загрузку с прокруткой
- Вторая схема, используемая для достижения крючков
1. Традиционное решение
Традиционное решение реализует процесс прокатки:
* 展示Loading...
* 判断该元素是否在视口中,是则展示真正的内容,否则进行下一步
* 递归获取滚动容器
* 给滚动容器添加滚动事件
* 当元素出现在视口中时,开始展示真正的内容,并取消监控事件
1.1 Создайте контейнер
Файл index.js напрямую использует код для инициализации проекта, Измените app.js, чтобы создать контейнер
import React from 'react';
import Scroll from './ScrollLoadSimple';
import './App.css';
const domNum = 20;
const App = () => {
return (
<div className="app">
{
Array.from(
{ length: domNum },
(text, index) => (
<Scroll text={`第${index + 1}个元素`} />
)
)
}
</div>
);
}
export default App;
1.2 Загрузка дисплея
Создайте папку ScrollLoadSimple и создайте index.js
В компоненте загрузки прокрутки нам нужно добавить поле в состояние, чтобы указать, что оно загружается, поэтому мы могли бы также использовать загрузку.
index.js
import React from 'react';
class ScrollLoad extends React.Component {
state = { loading: true }
render() {
const { loading } = this.state;
const { text } = this.props;
return (
<div className="scrollitem">
{
loading ? 'Loading...' : text
}
</div>
)
}
}
export default ScrollLoad;
style.css
.scrollitem {
height: 200px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid green;
margin-top: 10px;
}
.scrollitem:first-child {
margin-top: 0;
}
1.3 Определить, отображается ли элемент в окне просмотра
Есть также две схемы для оценки того, появляется ли элемент в области просмотра. Одна из них является самой простой схемой, которая использует offsetTop для вычисления расстояния от элемента body до элемента. Вычисление громоздко и чревато проблемами. относительно просто, используя getboundingclientrect для расчета элемента, должен ли он отображаться в окне просмотра.
1.3.1 Базовая схема Вычислить, появляется ли элемент в окне просмотра через offsetTop
Эта схема расчета настоятельно не рекомендуется, расчет громоздкий, и могут возникнуть проблемы, если контейнер прокрутки вложен.
Сначала получите offsetTop элемента,
Затем рекурсивно получить offsetTop родительского элемента, а сумма после сложения — это расстояние от левого верхнего угла области просмотра до элемента,
Затем получите контейнер прокрутки и получите высоту прокрутки через scrollTop.Условие контейнера прокрутки — scrollHeight > clientHeight. Возвращает null, если ни один из родительских элементов не удовлетворяет условию, а затем вычисляет высоту прокрутки, используяdocument.scrollingElement.scrollTop
, в хромеdocument.scrollingElement
даhtml
, а такжеdocument.documentElement
.
Сравнивая высоту экрана + высоту прокрутки и это расстояние, вы можете сказать, появляется ли элемент в области просмотра.
Здесь есть ямка, что offsetTop вычисляется на основе элемента-предка или тела, позиция которого является относительной,
Предполагая, что положение элемента A относительно,
Положение элемента B не является относительным, родительским узлом элемента B является элемент A, а offsetTop равен 36.
Узел C - это детский узел элемента B, и верх совпадает с элементом B, элементы представляют собой Offsettop C 36,
Следовательно, при рекурсивном получении offsetTop может использоваться только для позиционирования элементов относительно предка.
import React from 'react';
import './style.css';
class ScrollLoad extends React.Component {
state = { loading: true }
ref = React.createRef();
componentDidMount() {
const node = this.ref.current;
this.scrollParent = this.getScrollParent(node);
if (this.checkVisible(node)) {
this.setState({ loading: false });
}
}
getScrollParent = (node) => {
if (!node || node.parentNode === document.documentElement) {
return null;
}
const parentNode = node.parentNode;
if (parentNode.scrollHeight > parentNode.clientHeight
|| parentNode.scrollWidth > parentNode.clientWidth
) {
return parentNode;
}
return this.getScrollParent(parentNode);
}
checkVisible = (node) => {
let offsetTop = node.offsetTop;
let offsetLeft = node.offsetLeft;
let parentNode = node.parentNode;
while (parentNode && parentNode !== document.body) {
if (getComputedStyle(parentNode).position === 'relative') {
offsetTop += parentNode.offsetTop;
offsetLeft += parentNode.offsetLeft;
}
parentNode = parentNode.parentNode;
}
// 滚动元素在最外层时,计算scrollTop要使用scrollingElement
const scrollParent = this.scrollParent || document.scrollingElement;
return window.innerHeight + scrollParent.scrollTop > offsetTop
&& window.innerWidth + scrollParent.scrollLeft > offsetLeft;
}
render() {
const { loading } = this.state;
const { text } = this.props;
return (
<div className="scrollitem" ref={this.ref}>
{
loading ? 'Loading...' : text
}
</div>
)
}
}
export default ScrollLoad;
1.3.2 Используйте getboundingclientrect, чтобы вычислить, отображается ли он в области просмотра
Используйте getBoundingClientRect для получения информации об узле относительно области просмотра.
checkVisible
функция
checkVisible = (node) => {
if (node) {
const { top, bottom, left, right } = node.getBoundingClientRect();
return bottom > 0
&& top < window.innerHeight
&& left < window.innerWidth
&& right > 0;
}
return false;
}
Супер легко.
1.4 Получите контейнер прокрутки
В разделе 1.3.1getScrollTop
Функция состоит в том, чтобы получить контейнер прокрутки, поэтому я не буду здесь вдаваться в подробности.
1.5 Добавить событие прокрутки
Когда элемент не отображается в области просмотра, чтобы прослушать событие прокрутки контейнера прокрутки, Изменить didMount
componentDidMount() {
const node = this.ref.current;
this.scrollParent = this.getScrollParent(node);
if (this.checkVisible(node)) {
this.setState({ loading: false });
} else {
this.addEvent();
}
}
Добавьте событие прокрутки. Если контейнера прокрутки нет, добавьте событие прокрутки в окно.
Когда контейнер начнет прокручиваться, определите, появится ли он в области просмотра, если да, отобразите реальный контент и отмените событие прослушивания.
Поскольку частота событий прокрутки очень высока, используется функция дросселя, а здесь используется функция дросселя lodash.
См. код нижеimport throttle from 'lodash/throttle';
onScroll = throttle(() => {
const node = this.ref.current;
if (this.checkVisible(node)) {
this.setState({ loading: false });
this.cancelEvent();
}
}, 200)
addEvent = () => {
// 滚动元素在最外层时,要在window上添加滚动事件
const scrollParent = this.scrollParent || window;
scrollParent.addEventListener('scroll', this.onScroll);
}
cancelEvent = () => {
const scrollParent = this.scrollParent || window;
scrollParent.removeEventListener('scroll', this.onScroll);
}
Наконец, не забудьте отменить прослушивание в willUnmount
componentWillUnmount() {
this.cancelEvent();
}
2. Использование H5 API IntersectionObserver
2.1 Как реализовать
код напрямую
import React from 'react';
import './style.css';
class ScrollLoad extends React.Component {
state = { loading: true }
ref = React.createRef();
componentDidMount() {
const node = this.ref.current;
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.setState({ loading: false });
observer.unobserve(node);
}
})
});
this.observer.observe(node);
}
componentWillUnmount() {
this.observer.disconnect();
}
render() {
const { loading } = this.state;
const { text } = this.props;
return (
<div className="scrollitem" ref={this.ref}>
{
loading ? 'Loading...' : text
}
</div>
)
}
}
export default ScrollLoad;
Преимущества: IntersectionObserver имеет очень низкий приоритет, потребляет очень мало производительности и не требует большого количества суждений, с меньшим количеством кода и простой логикой.
IntersectionObserver может выполнять гораздо больше функций.
Но есть еще некоторые проблемы с совместимостью, вы можете использоватьpolyfill.
2.2 Версия хуков
import React from 'react';
import './style.css';
const ScrollLoad = ({ text }) => {
const [loading, setLoading] = React.useState(true);
const ref = React.createRef();
React.useEffect(() => {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setLoading(false);
observer.unobserve(entry.target);
}
});
});
observer.observe(ref.current);
return () => {
observer.disconnect();
}
});
return (
<div className="scrollitem" ref={ref}>
{
loading ? 'Loading...' : text
}
</div>
)
}
export default ScrollLoad;
3. Повторное использование кода
3.1 Повторное использование кода компонента высокого порядка
const ComponentWithScrollLoad = (Component) => {
return class extends React.Component {
state = { loading: true }
ref = React.createRef();
componentDidMount() {
const node = this.ref.current;
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.setState({ loading: false });
observer.unobserve(node);
}
})
});
this.observer.observe(node);
}
componentWillUnmount() {
this.observer.disconnect();
}
render() {
const { loading } = this.state;
if (loading) {
const loadingText = this.props.Loading || 'loading';
return (
<div ref={this.ref}>{loadingText}</div>
)
}
return <Component {...this.props} />
}
}
}
Как использовать:
import React from 'react';
import './style.css';
import ComponentWithScrollLoad from './ComponentWithScrollLoad';
const Component = ({ text }) => <div className="scrollitem">{text}</div>
export default ComponentWithScrollLoad(Component);
3.2 Повторное использование кода реквизита рендеринга
class ScrollLoad extends React.Component {
state = { loading: true }
ref = React.createRef();
componentDidMount() {
const node = this.ref.current;
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.setState({ loading: false });
observer.unobserve(node);
}
})
});
this.observer.observe(node);
}
componentWillUnmount() {
this.observer.disconnect();
}
render() {
const { loading } = this.state;
if (loading) {
const loadingText = this.props.Loading || 'loading';
return (
<div ref={this.ref}>{loadingText}</div>
)
}
return this.props.children;
}
}
Как использовать
import ScrollLoad from './ScrollLoad';
const Component = ({ text }) => (
<ScrollLoad>
<div className="scrollitem">{text}</div>
</ScrollLoad>
);
export default Component;
3.3 usehooks
useLoading.js
import React from 'react';
const useLoading = (ref) => {
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
if (!ref.current) {
return () => { }
}
const node = ref.current;
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setLoading(false);
observer.unobserve(entry.target);
}
});
});
if (node != null) {
observer.observe(node);
}
return () => {
observer.disconnect();
}
}, [ref]);
return loading;
}
export default useLoading;
Как использовать:
import useLoading from './useLoading';
const Component = ({ text }) => {
const ref = React.useRef(null);
const loading = useLoading(ref);
return (
<div className="scrollitem" ref={ref}>{loading ? 'loading' : text}</div>
)
}
export default Component;