React реализует загрузку прокрутки

React.js

предисловие

В проекте есть две страницы с большим количеством картинок, нарисованных Echarts.При входе он сильно застревает.После использования прокрутки загрузка стала намного плавнее,и пользовательский опыт значительно улучшился. Кроме того, существует множество других применений загрузки прокрутки, таких как: прокрутка страниц, беспроводная прокрутка и запросы на отображение изображений в области просмотра. . .
Прикрепите ссылку на гитхаб
содержание этой статьи

  1. Традиционное решение реализует прокатную загрузку
  2. H5 API IntersectionObserver реализует загрузку с прокруткой
  3. Вторая схема, используемая для достижения крючков

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;