如何在 React 中使用可变高度元素实现无限滚动

How to achieve infinite scrolling in React with variable height elements?

本文关键字:高度 元素 实现 滚动 无限 React      更新时间:2023-09-26

我正在尝试在 React (0.14)/Omniscient 应用程序中实现无限滚动。到目前为止,我一直在使用 react-infinity 库的修改版本,但它需要知道元素高度。如何使用动态元素高度实现无限滚动?

我已经部署了一个名为InfinitelyScrolling的演示应用程序,以显示我正在尝试做什么,其源代码位于GitHub上。这个应用程序演示了不同高度的元素,并且反应无穷大迫使每个元素具有一定的高度。

我修改后的 react-infinity 源代码实现了无限滚动,如下所示:

'use strict';
var React = require('react');
let s = require('react-prefixr')
function realRender(direction) {
  var windowWidth = this.state.windowWidth;
  var windowHeight = this.state.windowHeight;
  var elementHeight = this.props.mobileWidth <= windowWidth ? this.props.elementHeight :
    this.props.elementMobileHeight;
  var windowY, elementY;
  if (direction === 'vertical') {
    windowY = windowHeight;
    elementY = elementHeight;
  } else {
    windowY = windowWidth;
    elementY = elementWidth;
  }
  var numElements = 1;
  // Number of pixels the container has been scrolled from the top
  var scrollStart = this.state.scrollTop - this.props.scrollDelta;
  console.log(`Scrolled ${this.state.scrollTop} pixels from the top`)
  var numBefore = Math.floor(scrollStart / elementHeight);
  console.log(`Number of elements before scroll start: ${numBefore}`)
  var numVisible = Math.ceil((numBefore * elementY + windowY) / elementY);
  console.log(`Number of visible elements: ${numVisible}`)
  // Keep some extra elements before and after visible elements
  var extra = numElements === 1 ? Math.ceil(numVisible / 2) : 2;
  var lowerLimit = (numBefore - extra) * numElements;
  var higherLimit = (numVisible + extra * 2) * numElements;
  console.log(`Lower limit: ${lowerLimit}, higherLimit: ${higherLimit}, extra: ${extra}`)
  var elementsToRender = [];
  this.props.data.forEach(function (obj, index) {
    if (index >= lowerLimit && index < higherLimit) {
      console.log(`Rendering data item ${index}:`, obj)
      var column, row;
      if (direction === 'vertical') {
        column = index % numElements;
        row = Math.floor(index / numElements);
      } else {
        row = index % numElements;
        column = Math.floor(index / numElements);
      }
      var id = obj.id != null ? obj.id : obj._id;
      var yOffset = (row * elementHeight);
      var subContainer = SubContainer(
        {
          key: id,
          transform: 'translate(0, ' + yOffset + 'px)',
          height: elementHeight + 'px',
        },
        this.props.childComponent(obj)
      );
      elementsToRender.push(subContainer);
    } else {
      console.log(`Skipping data item ${index}`)
    }
  }.bind(this));
  return React.createElement(this.props.containerComponent,
    {
      className: 'infinite-container', style: {
        height: (elementHeight * Math.ceil(this.props.data.length / numElements)) + 'px',
        width: '100%',
        position: 'relative',
      },
    },
    elementsToRender
  );
}
var SubContainer = React.createFactory(React.createClass({
  displayName: 'Sub-Infinity',
  getInitialState: function () {
    return {
      transform: this.props.transform + ' scale(1)',
      opacity: '0',
    };
  },
  componentDidMount: function (argument) {
    this.setState({transform: this.props.transform + ' scale(1)', opacity: '1',});
  },
  componentWillReceiveProps: function (newProps) {
    this.setState({transform: newProps.transform + ' scale(1)',});
  },
  componentWillEnter: function (cb) {
    this.setState({transform: this.props.transform + ' scale(1)', opacity: '0',});
    setTimeout(cb, 100);
  },
  componentDidEnter: function () {
    this.setState({transform: this.props.transform + ' scale(1)', opacity: '1',});
  },
  componentWillLeave: function (cb) {
    this.setState({transform: this.props.transform + ' scale(1)', opacity: '0',});
    setTimeout(cb, 400);
  },
  render: function () {
    return React.DOM.div({style: s({
      position: 'absolute',
      top: '0',
      left: '0',
      transform: this.state.transform,
      width: this.props.width,
      height: this.props.height,
      transition: this.props.transition,
      opacity: this.state.opacity,
    }),},
      this.props.children
    );
  },
}));
var Infinite = React.createClass({
  displayName: 'React-Infinity',
  getDefaultProps: function () {
    return {
      data: [],
      maxColumns: 100,
      transition: '0.5s ease',
      id: null,
      className: 'infinite-container',
      elementClassName: '',
      component: 'div',
      containerComponent: 'div',
      mobileWidth: 480,
      justifyOnMobile: true,
      scrollDelta: 0,
      direction: 'vertical',
      preRender: false,
    };
  },
  propTypes: {
    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
    maxColumns: React.PropTypes.number,
    id: React.PropTypes.string,
    className: React.PropTypes.string,
    elementHeight: React.PropTypes.number,
    mobileWidth: React.PropTypes.number,
    elementMobileHeight: React.PropTypes.number,
    elementMobileWidth: React.PropTypes.number,
    justifyOnMobile: React.PropTypes.bool,
    preRender: React.PropTypes.bool,
    scrollDelta: React.PropTypes.number,
  },
  getInitialState: function () {
    return {
      scrollTop: 0,
      windowWidth: this.props.windowWidth || 800,
      windowHeight: this.props.windowHeight || 600,
      loaded: false,
      extra: {
        count: 0,
      },
    };
  },
  componentDidMount: function () {
    global.addEventListener('resize', this.onResize);
    global.addEventListener('scroll', this.onScroll);
    this.onScroll()
    this.setState({
      loaded: true,
      windowWidth: global.innerWidth,
      windowHeight: global.innerHeight,
      elementHeight: this.props.elementHeight ||
        this.refs.element1.getDOMNode().getClientRects()[0].height,
      scrollTop: global.scrollY || 0,
    });
  },
  onScroll: function () {
    var scrollTop = global.scrollY;
    if (this.state.scrollTop !== scrollTop) {
      this.setState({scrollTop: scrollTop,});
    }
  },
  onResize: function () {
    this.setState({windowHeight: global.innerHeight, windowWidth: global.innerWidth,});
  },
  componentWillUnmount: function () {
    global.removeEventListener('resize', this.onResize);
    global.removeEventListener('scroll', this.onScroll);
  },
  render: function(){
    if (!this.state.loaded) {
      return this.props.preRender ? React.createElement(this.props.containerComponent,
        {
          className: this.props.className,
          id: this.props.id,
          style: {
            fontSize: '0',
            position: 'relative',
          },
        }, this.props.data.map(function (elementData, i) {
          return React.createElement(this.props.component, {
            style: {display: 'inline-block', margin: '32px', verticalAlign: 'top',},
          }, React.createElement(this.props.childComponent, elementData));
        }.bind(this)))
        : null;
    }
    var direction = this.props.direction;
    if (direction !== 'horizontal' && direction !== 'vertical') {
      direction = 'vertical';
      console.warn('the prop `direction` must be either "vertical" or "horizontal". It is set to',
        direction);
    }
    return realRender.call(this, direction);
  },
});
module.exports = Infinite;

我已经在一个 flex 容器中使用多个砖石组件(每个组件都有可变的项目高度、来自互联网的图像)实现了无限滚动。

我使用反应列表来做到这一点(https://github.com/orgsync/react-list)。

这是一个例子:

import loadAccount from 'my-account-loader';
import React from 'react';
import ReactList from 'react-list';
class MyComponent extends React.Component {
  state = {
    accounts: []
  };
  componentWillMount() {
    loadAccounts(this.handleAccounts);
  }
  handleAccounts(accounts) {
    this.setState({accounts});
  }
  renderItem(index, key) {
    return <div key={key}>{this.state.accounts[index].name}</div>;
  }
  render() {
    return (
      <div>
        <h1>Accounts</h1>
        <div style={{overflow: 'auto', maxHeight: 400}}>
          <ReactList
            itemRenderer={this.renderItem}
            length={this.state.accounts.length}
            type='variable'
          />
        </div>
      </div>
    );
  }
}

如果元素高度可变,则必须将类型设置为 variable