react-lazyload 源码解析

前言

早在多年前,lazyload 已经出现了,懒加载在前端里边同样具有十分重要的意义。react-lazyload 的作用是当组件未出现在屏幕内时,不去挂载该组件,而是使用 placeholder 去渲染,让滚动使内容出现后,组件会被挂载。就是这么简单!例如,一个复杂的组件(非首屏内容),使用了懒加载后,渲染首屏就会节省很多资源,从而减少首屏渲染时间。

Demo

源码地址 react-lazyload
Demo地址 Demo

HelloWorld

将需要懒加载的组件使用 LazyLoad 包裹即可,最好使用 height 进行站位,否则该组件位置将会为 0

    
         /*
                                  Lazy loading images is supported out of box,
                                  no extra config needed, set `height` for better
                                  experience
                                 */
      

解析

从源码角度分析~

一览核心

本小节摘取了最核心的代码,目的在于对 LazyLoad 组件有个最核心的认识,它的核心就是监听滚动事件,检查组件是否在屏幕内,如果在的话就显示,不在的话就不显示~

class LazyLoad extends Component {
  componentDidMount() {
   on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
  }

  render() {
    return this.visible ?
           this.props.children :
             this.props.placeholder ?
                this.props.placeholder :
                
; } }

LazyLoad 的属性,透过属性,我们可以知道它大概有些什么功能。

LazyLoad.propTypes = {
  once: PropTypes.bool,
  height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  offset: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]),
  overflow: PropTypes.bool, // 不是 window 滚动,而使用了 overflow: scroll 
  resize: PropTypes.bool, // 是否监听 resize
  scroll: PropTypes.bool, // 是否监听滚动
  children: PropTypes.node,
  throttle: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
  debounce: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
  placeholder: PropTypes.node,
  scrollContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  unmountIfInvisible: PropTypes.bool,
  preventLoading: PropTypes.bool
};
// 默认值
LazyLoad.defaultProps = {
  once: false,
  offset: 0,
  overflow: false,
  resize: false,
  scroll: true,
  unmountIfInvisible: false,
  preventLoading: false,
};

完整的 componentDidMount,scrollport 是滚动试图,默认是 window,如果 props 传入了 scrollContainer,那么滚动试图将是自定义的。needResetFinalLazyLoadHandler 是控制是否重置滚动监听。debounce 和 throttle 分别是用来控制滚动事件的监听触发频率,默认都是 undefine,needResetFinalLazyLoadHandler 初始值为 false。finalLazyLoadHandler 初始值也为 undefine,而 overflow 也为 false,scroll 为 true,listeners 是需要懒加载的组件集合,初始大小肯定为0,componentDidMount 最后才会进行添加,因此最终会走到 **on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent),事件只需要一次绑定即可。
**

componentDidMount() {
    // It's unlikely to change delay type on the fly, this is mainly
    // designed for tests
    let scrollport = window;
    const {
      scrollContainer,
    } = this.props;
    if (scrollContainer) {
      if (isString(scrollContainer)) {
        scrollport = scrollport.document.querySelector(scrollContainer);
      }
    }
    const needResetFinalLazyLoadHandler = (this.props.debounce !== undefined && delayType === 'throttle')
      || (delayType === 'debounce' && this.props.debounce === undefined);

    if (needResetFinalLazyLoadHandler) {
      off(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
      off(window, 'resize', finalLazyLoadHandler, passiveEvent);
      finalLazyLoadHandler = null;
    }

    if (!finalLazyLoadHandler) {
      if (this.props.debounce !== undefined) {
        finalLazyLoadHandler = debounce(lazyLoadHandler, typeof this.props.debounce === 'number' ?
                                                         this.props.debounce :
                                                         300);
        delayType = 'debounce';
      } else if (this.props.throttle !== undefined) {
        finalLazyLoadHandler = throttle(lazyLoadHandler, typeof this.props.throttle === 'number' ?
                                                         this.props.throttle :
                                                         300);
        delayType = 'throttle';
      } else {
        finalLazyLoadHandler = lazyLoadHandler;
      }
    }

    if (this.props.overflow) {
      const parent = scrollParent(this.ref);
      if (parent && typeof parent.getAttribute === 'function') {
        const listenerCount = 1 + (+parent.getAttribute(LISTEN_FLAG));
        if (listenerCount === 1) {
          parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent);
        }
        parent.setAttribute(LISTEN_FLAG, listenerCount);
      }
    } else if (listeners.length === 0 || needResetFinalLazyLoadHandler) {
      const { scroll, resize } = this.props;

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

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

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

通常 finalLazyLoadHandler 就是 lazyLoadHandler,不会对滚动事件进行 debounce 或 throttle,我们一般为了性能,会使用 throttle 进行处理。函数会对每一个懒加载组件进行 checkVisible,之后会移除 once component

const lazyLoadHandler = () => {
  for (let i = 0; i < listeners.length; ++i) {
    const listener = listeners[i];
    checkVisible(listener);
  }
  // Remove `once` component in listeners
  purgePending();
};

checkVisible,检查组件是否出现在 viewport 中,如果出现了就吧 visible 设置为 true,当然如果设置了 unmountIfInvisible = true,那么不可见时组件将被移除,如果之前已经渲染了,需要避免再次渲染。

const checkVisible = function checkVisible(component) {
  const node = component.ref;
  if (!(node instanceof HTMLElement)) {
    return;
  }

  const parent = scrollParent(node);
  const isOverflow = component.props.overflow &&
                     parent !== node.ownerDocument &&
                     parent !== document &&
                     parent !== document.documentElement;
  const visible = isOverflow ?
                  checkOverflowVisible(component, parent) :
                  checkNormalVisible(component);
  if (visible) {
    // Avoid extra render if previously is visible
    if (!component.visible && !component.preventLoading) {
      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();
    }
  }
};

checkNormalVisible 检查组件是否 visible 的函数,判断组件的getgetBoundingClientRect 的 top - offset(相对于屏幕顶部的距离) 与 window 的 height 之间的关系

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;

  try {
    // 这个语法 node 也是支持的
    ({ top, height: elementHeight } = node.getBoundingClientRect());
  } catch (e) {
    ({ top, height: elementHeight } = defaultBoundingClientRect);
  }

  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);
};
(top - offsets[0] <= windowInnerHeight) &&
         (top + elementHeight + offsets[1] >= 0);  

一张图解析!


react-lazyload 源码解析_第1张图片
check visible example

到这里解析的差不多了

欣赏一下 throttle

export default function throttle(fn, threshhold, scope) {
  threshhold || (threshhold = 250);
  var last,
      deferTimer;
  return function () {
    var context = scope || this;

    var now = +new Date,
        args = arguments;
    if (last && now < last + threshhold) {
      // hold on to it
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fn.apply(context, args);
      }, threshhold);
    } else {
      last = now;
      fn.apply(context, args);
    }
  };
}

再欣赏一下 debounce

export default function debounce(func, wait, immediate) {
  let timeout;
  let args;
  let context;
  let timestamp;
  let result;

  const later = function later() {
    const last = +(new Date()) - timestamp;

    if (last < wait && last >= 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) {
          context = null;
          args = null;
        }
      }
    }
  };

  return function debounced() {
    context = this;
    args = arguments;
    timestamp = +(new Date());

    const callNow = immediate && !timeout;
    if (!timeout) {
      timeout = setTimeout(later, wait);
    }

    if (callNow) {
      result = func.apply(context, args);
      context = null;
      args = null;
    }

    return result;
  };
}

获取 scrollParent

export default (node) => {
  if (!(node instanceof HTMLElement)) {
    return document.documentElement;
  }

  const excludeStaticParent = node.style.position === 'absolute';
  const overflowRegex = /(scroll|auto)/;
  let parent = node;

  while (parent) {
    if (!parent.parentNode) {
      return node.ownerDocument || document.documentElement;
    }

    const style = window.getComputedStyle(parent);
    const position = style.position;
    const overflow = style.overflow;
    const overflowX = style['overflow-x'];
    const overflowY = style['overflow-y'];

    if (position === 'static' && excludeStaticParent) {
      parent = parent.parentNode;
      continue;
    }

    if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) {
      return parent;
    }

    parent = parent.parentNode;
  }

  return node.ownerDocument || node.documentElement || document.documentElement;
};

总结思考

我们可以看到,Lazyload 并不能实现类似客户端的图片懒加载,Lazyload 加载图片也会出现白屏时间,解决办法是使用 image.onload,当图片资源请求关闭后,再显示图片,就可以做到类似客户端的效果。

你可能感兴趣的:(react-lazyload 源码解析)