美文网首页
react-lazyload 源码解析

react-lazyload 源码解析

作者: 没有颜色的菜 | 来源:发表于2019-07-28 11:59 被阅读0次

    前言

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

    Demo

    源码地址 react-lazyload
    Demo地址 Demo

    HelloWorld

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

        <LazyLoad height={200}>
            <img src="tiger.jpg" /> /*
                                      Lazy loading images is supported out of box,
                                      no extra config needed, set `height` for better
                                      experience
                                     */
          </LazyLoad>
    

    解析

    从源码角度分析~

    一览核心

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

    class LazyLoad extends Component {
      componentDidMount() {
       on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
      }
    
      render() {
        return this.visible ?
               this.props.children :
                 this.props.placeholder ?
                    this.props.placeholder :
                    <div style={{ height: this.props.height }} className="lazyload-placeholder" ref={this.setRef} />;
      }
    }
    

    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);  
    

    一张图解析!


    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 源码解析

          本文链接:https://www.haomeiwen.com/subject/iwjerctx.html