资讯专栏INFORMATION COLUMN

react-lazy-load粗读

dailybird / 2211人阅读

摘要:粗读近来没什么特别要做的事,下班回来的空闲时间也比较多,所以抽空看看懒加载是怎么实现的,特别是看了下的库的实现。之先别关注,按他给注释说测试用。之是组件绑定事件时会触发的函数。

react-lazy-load粗读

近来没什么特别要做的事,下班回来的空闲时间也比较多,所以抽空看看懒加载是怎么实现的,特别是看了下 react-lazy-load 的库的实现。

懒加载

这里懒加载场景不是路由分割打包那种,而是单个页面中有一个很长的列表,列表中的图片进行懒加载的效果。

jquery 时代,这种列表图片懒加载效果就已经有了,那么我们想一想这种在滚动的时候才去加载图片等资源的方式该如何去实现呢?

大致原理

浏览器解析 html 的时候,在遇到 img 标签以及发现 src 属性的时候,浏览器就会去发请求拿图片去了。这里就是切入点,根据这种现象,做下面几件事:

把列表中所有的图片的 img 标签的 src 设为空

把真实的图片路径存成一个 dom 属性,打个比方:

写一个检测列表某一项是否是可见状态

全局滚动事件做一个监听,检测当前列表的项是否是可见的,如果可见则给 img 标签上存着真实图片路径赋值给 src 属性

react-lazy-load

知道懒加载的大概原理,来看一下 react-lazy-load 是怎么做的。

大体看了下 react-lazy-load 的实现的总体思路就更加简单了,本质上就是让需要懒加载的组件包含在这个包提供的 LazyLoad 组件中,不渲染这个组件,然后去监听这个 LazyLoad 组件是否已经是可见了,如果是可见了那么就去强制渲染包含在 LazyLoad 组件内部需要懒加载的组件了。

这种方式相较于手动去控制 img 标签来的实在是太方便了,完全以组件为单位,对组件进行懒加载。这样的话,完全就不需要感知组件内部的逻辑和渲染逻辑,无论这个需要懒加载的组件内部是有几个 img 标签,也完全不用去手动操控 src 属性的赋值。

react-lazy-load 之 render
class LazyLoad extends React.Component{
    constructor(props) {
        super(props)
        this.visible = false
    }
    componentDidMount() {
        // 主要是监听事件
        // 省略此处代码
    }
    shouldComponentUpdate() {
        return this.visible
    }
    componentWillUnmount() {
        // 主要是移除监听事件
        // 省略
    }
    render () {
        return this.visible
                ? this.props.children
                : this.props.placeholder
                    ? this.props.placeholder
                    : 
} }

render 函数能够看出来,依据当前 visible 的值来确定是否渲染 this.props.children,如果为 false 则去渲染节点的占位符。如果外部传入一个占位节点,就用这个传入的占位节点,否则就用默认的占位符去占位。注意到:shouldComponentUpdate 依据 this.visible 的值去判断是否更新组件。剩下的,该去看看如何监听事件以及修改 this.visible、强制重新渲染组件的。

react-lazy-load 之 componentDidMount
  componentDidMount() {
    // It"s unlikely to change delay type on the fly, this is mainly
    // designed for tests
    const needResetFinalLazyLoadHandler = (this.props.debounce !== undefined && delayType === "throttle")
      || (delayType === "debounce" && this.props.debounce === undefined);

    if (needResetFinalLazyLoadHandler) {
      off(window, "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(ReactDom.findDOMNode(this));
      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(window, "scroll", finalLazyLoadHandler, passiveEvent);
      }

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

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

needResetFinalLazyLoadHandler 先别关注,按他给注释说测试用。 finalLazyLoadHandler 依据外部 debouncethrottle 来选择是防抖还是节流还是都不用。根据外部传入的overflow 来确定是否是在某一个节点中 overflow 的下拉框的懒加载还是普通的整个 window 的懒加载。然后就是依据是 scroll 还是 resize 来给 window 增加监听事件 finalLazyLoadHandler。 最后就是把这个组件实例放到了 listeners 这个数组里,然后调用 checkVisible 检查是否可见。

react-lazy-load 之 checkVisible
/**
 * 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 = ReactDom.findDOMNode(component);
  if (!node) {
    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) {
      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();
    }
  }
};

parent 就是找到这个组件的上层组件的 dom 节点,通过 checkOverflowVisiblecheckNormalVisible这两个函数拿到该节点是否在可视区域内得到 visible。然后依据 visible的值修改 componentvisible的值,然后调用组件的 forceUpdate 方法,强制让组件重新渲染。主要到组件的 visible 并不是挂载到 state 上,所以这里不是用 setState 来重新渲染。

react-lazy-load 之 checkNormalVisible
/**
 * Check if `component` is visible in document
 * @param  {node} component React component
 * @return {bool}
 */
const checkNormalVisible = function checkNormalVisible(component) {
  const node = ReactDom.findDOMNode(component);

  // 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 {
    ({ 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);
};

主要逻辑就是拿到组件的 dom 节点的 getBoundingClientRect 返回值和 window.innerHeight 进行比较来判断是否是在可视范围内。这里在比较的时候还有个 component.props.offset 也参与了比较,说明设置了 offset 的时候,组件快要出现在可视范围的时候就会去重新渲染组件而不是出现在可视范围内才去重新渲染。

react-lazy-load 之 lazyLoadHandler

lazyLoadHandler 是组件绑定事件时会触发的函数。

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

每次监听事件执行的时候,都去检查一下组件,如果满足条件就去强制渲染组件。

react-lazy-load 之 componentWillUnmount
 componentWillUnmount() {
    if (this.props.overflow) {
      const parent = scrollParent(ReactDom.findDOMNode(this));
      if (parent && typeof parent.getAttribute === "function") {
        const listenerCount = (+parent.getAttribute(LISTEN_FLAG)) - 1;
        if (listenerCount === 0) {
          parent.removeEventListener("scroll", finalLazyLoadHandler, passiveEvent);
          parent.removeAttribute(LISTEN_FLAG);
        } else {
          parent.setAttribute(LISTEN_FLAG, listenerCount);
        }
      }
    }

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

    if (listeners.length === 0) {
      off(window, "resize", finalLazyLoadHandler, passiveEvent);
      off(window, "scroll", finalLazyLoadHandler, passiveEvent);
    }
  }

组件卸载的时候,把一些绑定事件解绑一下,细节也不说了。

总结

抛开 react-lazy-load 一些实现细节,从总体把握整个懒加载的过程,其实懒加载的原理并不难。当时我也看了一下 vue 那边的 vue-lazyLoad 这个库想写一个对比的文章,我以为这个 vue 库的内容会写的和 react-lazy-load 差不多,结果发现 vue-lazyLoad 代码很长而且好像比较复杂,所以也就没看了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108247.html

相关文章

  • snabbdom源码粗读

    摘要:这个大概是的钩子吧在每一次插入操作的时候都将节点这类型方法可以看出来是在调用对应的方法因为开始的时候就导入进来了插入节点操作的时候都需要加入子节点有子元素也就是的时候递归调用循环子节点生成对应着一些操作之后都要触发钩子函数。 snabbdom 本文的snabbdom源码分析采用的是0.54版本(即未用ts重写前的最后一版) 前期了解 snabbdom被用作vue的虚拟dom。本文的一个...

    svtter 评论0 收藏0
  • 从零开始的WEB框架——感悟

    摘要:读了周勇老师的从零开始写框架,感觉干货还是挺多的。不过,这本书中的从零开始并不是指的零基础,而是从无到有。还是先说说目前的感受吧。第五章讲了的优化文件上传和下载集成安全框架和框架。如果大家看了这本书有什么新的感悟,也欢迎分享给我。 读了周勇老师的《从零开始写javaweb框架》,感觉干货还是挺多的。想把自己的收获分享给大家。不过,这本书中的从零开始并不是指的零基础,而是从无到有。所以,...

    MRZYD 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<