react lazyload学习

131 阅读2分钟

lazyload组件

lazyload有着提升网页加载速度,减少首屏不必要请求的功能。另外关于业务提出的希望卡片根据是否在可视区域内进行的精确埋点上报也可以利用到lazyload组件,但需要做一些修改。 react lazyload的原理就是判断被包装的子组件是否在可视区域内,来判断是否需要展示这个组件。

代码的核心结构

我们简化一下,只考虑可滚动区域为window。另外我们不考虑节流的问题。那代码可以分为两个部分

组件本身的代码

class LazyLoad extends Component {
  constructor(props) {
    super(props);

    this.visible = false;
    this.setRef = this.setRef.bind(this);
  }

  componentDidMount() {
    // It's unlikely to change delay type on the fly, this is mainly
    // designed for tests
    let scrollport = window;

    finalLazyLoadHandler = lazyLoadHandler;
    if (listeners.length === 0) {
      const { scroll, resize } = this.props;

      if (scroll) {
        on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
      }

      if (resize) {
        on(window, 'resize', finalLazyLoadHandler, passiveEvent);
      }
    }

    listeners.push(this);
    checkVisible(this);
  }

  shouldComponentUpdate() {
    return this.visible;
  }

  componentWillUnmount() {

    const index = listeners.indexOf(this);
    if (index !== -1) {
      listeners.splice(index, 1);
    }

    if (listeners.length === 0 && typeof window !== 'undefined') {
      off(window, 'resize', finalLazyLoadHandler, passiveEvent);
      off(window, 'scroll', finalLazyLoadHandler, passiveEvent);
    }
  }

  setRef(element) {
    if (element) {
      this.ref = element;
    }
  }

  render() {
    const {
      height,
      children,
      placeholder,
      className,
      classNamePrefix,
      style
    } = this.props;

    return (
      <div className={`${classNamePrefix}-wrapper ${className}`} ref={this.setRef} style={style}>
        {this.visible ? (
          children
        ) : placeholder ? (
          placeholder
        ) : (
          <div
            style={{ height: height }}
            className={`${classNamePrefix}-placeholder`}
          />
        )}
      </div>
    );
  }
}

const getDisplayName = WrappedComponent =>
  WrappedComponent.displayName || WrappedComponent.name || 'Component';

const decorator = (options = {}) =>
  function lazyload(WrappedComponent) {
    return class LazyLoadDecorated extends Component {
      constructor() {
        super();
        this.displayName = `LazyLoad${getDisplayName(WrappedComponent)}`;
      }

      render() {
        return (
          <LazyLoad {...options}>
            <WrappedComponent {...this.props} />
          </LazyLoad>
        );
      }
    };
  };

export { decorator as lazyload };
export default LazyLoad;
  • render部分就是判断了一下visible属性,来处理子组件是否需要被渲染
  • componentDidMount listeners是所有lazyload实例的回调实例队列,绑定lazyLoadHandler,如果之前没有过事件监听,就需要在window上监听一下滚动和resize事件。关于checkVisible我们一会再看。
  • shouldComponentUpdate 性能优化 如果visible不为真 显然是不需要进行任何刷新的
  • componentWillUnmount 实例从listeners删除,并且如果listeners为空了,需要把windows上的事件清理了

判断函数

/**
 * Check if `component` is visible in document
 * @param  {node} component React component
 * @return {bool}
 */
const checkNormalVisible = function checkNormalVisible(component) {
  const node = component.ref;

  // If this element is hidden by css rules somehow, it's definitely invisible
  if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
    return false;

  let top;
  let elementHeight;
  ({ top, height: elementHeight } = node.getBoundingClientRect());

  const windowInnerHeight =
    window.innerHeight || document.documentElement.clientHeight;

  const offsets = Array.isArray(component.props.offset)
    ? component.props.offset
    : [component.props.offset, component.props.offset]; // Be compatible with previous API

  return (
    top - offsets[0] <= windowInnerHeight &&
    top + elementHeight + offsets[1] >= 0
  );
};

/**
 * Detect if element is visible in viewport, if so, set `visible` state to true.
 * If `once` prop is provided true, remove component as listener after checkVisible
 *
 * @param  {React} component   React component that respond to scroll and resize
 */
const checkVisible = function checkVisible(component) {
  const node = component.ref;
  if (!(node instanceof HTMLElement)) {
    return;
  }
  const visible = checkNormalVisible(component);
  if (visible) {
    // Avoid extra render if previously is visible
    if (!component.visible) {
      if (component.props.once) {
        pending.push(component);
      }

      component.visible = true;
      component.forceUpdate();
    }
  } else if (!(component.props.once && component.visible)) {
    component.visible = false;
    if (component.props.unmountIfInvisible) {
      component.forceUpdate();
    }
  }
};

const lazyLoadHandler = () => {
  for (let i = 0; i < listeners.length; ++i) {
    const listener = listeners[i];
    checkVisible(listener);
  }
};

  • 首先看一下lazyLoadHandler,也就是滚动事件触发时的判断需要判断listeners所有组件实例的checkVisible结果
  • checkVisible的计算是在checkNormalVisible中,用component的节点的getBoundingClientRect的top值和height值,去和视窗的高做判断,top小于等于视窗高端或者top+height大于等于0,就可以认定在可视区域内。此时直接去修改component的visible值并且进行forceUpdate后,组件就会展示出来了