资讯专栏INFORMATION COLUMN

[源码阅读]通过react-infinite-scroller理解滚动加载要点

cikenerd / 2796人阅读

摘要:看它的源码主要意义不在知道如何使用它,而是知道以后处理滚动加载要注意的东西。通过判断不为的情况,确保滚动组件正常显示和在无滚动的情况下,和相等,都为在有滚动的情况下,表示实际内容高度,表示视口高度。

react-infinite-scroller就是一个组件,主要逻辑就是addEventListener绑定scroll事件。

看它的源码主要意义不在知道如何使用它,而是知道以后处理滚动加载要注意的东西。

此处跳到总结。
初识

参数:

// 渲染出来的DOM元素name
element: "div",
// 是否能继续滚动渲染
hasMore: false,
// 是否在订阅事件的时候执行事件
initialLoad: true,
// 表示当前翻页的值(每渲染一次递增)
pageStart: 0,
// 传递ref,返回此组件渲染的 DOM
ref: null,
// 触发渲染的距离
threshold: 250,
// 是否在window上绑定和处理距离
useWindow: true,
// 是否反向滚动,即到顶端后渲染
isReverse: false,
// 是否使用捕获模式
useCapture: false,
// 渲染前的loading组件
loader: null,
// 自定义滚动组件的父元素
getScrollParent: null,
深入 componentDidMount
componentDidMount() {
  this.pageLoaded = this.props.pageStart;
  this.attachScrollListener();
}

执行attachScrollListener

attachScrollListener
attachScrollListener() {
  const parentElement = this.getParentElement(this.scrollComponent);
  
  if (!this.props.hasMore || !parentElement) {
    return;
  }

  let scrollEl = window;
  if (this.props.useWindow === false) {
    scrollEl = parentElement;
  }
  scrollEl.addEventListener(
    "mousewheel",
    this.mousewheelListener,
    this.props.useCapture,
  );
  scrollEl.addEventListener(
    "scroll",
    this.scrollListener,
    this.props.useCapture,
  );
  scrollEl.addEventListener(
    "resize",
    this.scrollListener,
    this.props.useCapture,
  );
  
  if (this.props.initialLoad) {
    this.scrollListener();
  }
}

此处通过getParentElement获取父组件(用户自定义父组件或者当前dom的parentNode)

然后绑定了3个事件,分别是scroll,resize,mousewheel

前2种都绑定scrollListenermousewheel是一个非标准事件,是不建议在生产模式中使用的。

那么这里为什么要使用呢?

mousewheel解决chrome的等待bug

此处的mousewheel事件是为了处理chrome浏览器的一个特性(不知道是否是一种bug)。

stackoverflow:Chrome的滚动等待问题

上面这个问题主要描述,当在使用滚轮加载,而且加载会触发ajax请求的时候,当滚轮到达底部,会出现一个漫长而且无任何动作的等待(长达2-3s)。

window.addEventListener("mousewheel", (e) => {
    if (e.deltaY === 1) {
        e.preventDefault()
    }
})

以上绑定可以消除这个"bug"。

个人并没有遇到过这种情况,不知道是否有遇到过可以说说解决方案。
getParentElement
getParentElement(el) {
  const scrollParent =
    this.props.getScrollParent && this.props.getScrollParent();
  if (scrollParent != null) {
    return scrollParent;
  }
  return el && el.parentNode;
}

上面用到了getParentElement,很好理解,使用用户自定义的父组件,或者当前组件DOM.parentNode

scrollListener
scrollListener() {
  const el = this.scrollComponent;
  const scrollEl = window;
  const parentNode = this.getParentElement(el);

  let offset;
  // 使用window的情况
  if (this.props.useWindow) {
    const doc = document.documentElement || document.body.parentNode || document.body;
    const scrollTop = scrollEl.pageYOffset !== undefined
        ? scrollEl.pageYOffset
        : doc.scrollTop;
    // isReverse指 滚动到顶端,load新组件
    if (this.props.isReverse) {
      // 相反模式获取到顶端距离
      offset = scrollTop;
    } else {
      // 正常模式则获取到底端距离
      offset = this.calculateOffset(el, scrollTop);
    }
    // 不使用window的情况
  } else if (this.props.isReverse) {
    // 相反模式组件到顶端的距离
    offset = parentNode.scrollTop;
  } else {
    // 正常模式组件到底端的距离
    offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
  }

  // 此处应该要判断确保滚动组件正常显示
  if (
    offset < Number(this.props.threshold) &&
    (el && el.offsetParent !== null)
  ) {
    // 卸载事件
    this.detachScrollListener();
    // 卸载事件后再执行 loadMore
    if (typeof this.props.loadMore === "function") {
      this.props.loadMore((this.pageLoaded += 1));
    }
  }
}

组件核心。

几个学习/复习点

offsetParent

offsetParent返回一个指向最近的包含该元素的定位元素.

offsetParent很有用,因为计算offsetTopoffsetLeft都是相对于offsetParent边界的。

ele.offsetParent为 null 的3种情况:

ele 为body

ele 的positionfixed

ele 的displaynone

此组件中offsetParent处理了2种情况

useWindow的情况下(即事件绑定在window,滚动作用在body)

通过递归获取offsetParent到达顶端的高度(offsetTop)。

calculateTopPosition(el) {
 if (!el) {
   return 0;   
 }
 return el.offsetTop + this.calculateTopPosition(el.offsetParent);   
}

通过判断offsetParent不为null的情况,确保滚动组件正常显示

  if (
    offset < Number(this.props.threshold) &&
    (el && el.offsetParent !== null)
  ) {/* ... */ }

scrollHeightclientHeight

在无滚动的情况下,scrollHeightclientHeight相等,都为height+padding*2

在有滚动的情况下,scrollHeight表示实际内容高度,clientHeight表示视口高度。

每次执行loadMore前卸载事件。

确保不会重复(过多)执行loadMore,因为先卸载事件再执行loadMore,可以确保在执行过程中,scroll事件是无效的,然后再每次componentDidUpdate的时候重新绑定事件。

render
render() {
  // 获取porps
  const renderProps = this.filterProps(this.props);
  const {
    children,
    element,
    hasMore,
    initialLoad,
    isReverse,
    loader,
    loadMore,
    pageStart,
    ref,
    threshold,
    useCapture,
    useWindow,
    getScrollParent,
    ...props
  } = renderProps;

  // 定义一个ref
  // 能将当前组件的DOM传出去
  props.ref = node => {
    this.scrollComponent = node;
    // 执行父组件传来的ref(如果有)
    if (ref) {
      ref(node);
    }
  };

  const childrenArray = [children];
  // 执行loader
  if (hasMore) {
    if (loader) {
      isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader);
    } else if (this.defaultLoader) {
      isReverse
        ? childrenArray.unshift(this.defaultLoader)
        : childrenArray.push(this.defaultLoader);
    }
  }
  // ref 传递给 "div"元素
  return React.createElement(element, props, childrenArray);
}

这里一个小亮点就是,在react中,this.props是不允许修改的。

这里使用了解构

getScrollParent,
...props
} = renderProps;

这里解构相当于Object.assign,定义了一个新的object,便可以添加属性了,并且this.props不会受到影响。

总结

react-infinite-scroller逻辑比较简单。

一些注意/学习/复习点:

Chrome的一个滚动加载请求的bug。本文位置

offsetParent的一些实际用法。本文位置

通过不断订阅和取消事件绑定让滚动执行函数不会频繁触发。本文位置

scrollHeightclientHeight区别。本文位置

此库建议使用在自定义的一些组件上并且不那么复杂的逻辑上。

用在第三方库可以会无法获取正确的父组件,而通过document.getElementBy..传入。

面对稍微复杂的逻辑,

例如,一个搜索组件,订阅onChange事件并且呈现内容,搜索"a",对呈现内容滚动加载了3次,再添加搜索词"b",这时候"ab"的内容呈现是在3次之后。

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

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

相关文章

  • 给Ant Design list列表增加滑动框功能

    摘要:描述最近在用框架写一个项目遇到了一个小问题列表会加载出很多数据需要在固定区域查看所有的列表数据需求给列表增加一个滑动框开始在官网上看了下例子比较复杂是结合实现滚动自动加载列表。 描述:最近在用Ant Design 框架写一个项目,遇到了一个小问题,list列表会加载出很多数据.需要在固定区域查看所有的列表数据.需求:给list列表增加一个滑动框 开始在官网上看了下例子.比较复杂..是l...

    ISherry 评论0 收藏0
  • 深入理解js

    摘要:详解十大常用设计模式力荐深度好文深入理解大设计模式收集各种疑难杂症的问题集锦关于,工作和学习过程中遇到过许多问题,也解答过许多别人的问题。介绍了的内存管理。 延迟加载 (Lazyload) 三种实现方式 延迟加载也称为惰性加载,即在长网页中延迟加载图像。用户滚动到它们之前,视口外的图像不会加载。本文详细介绍了三种延迟加载的实现方式。 详解 Javascript十大常用设计模式 力荐~ ...

    caikeal 评论0 收藏0
  • Vue下滚动到页面底部无限加载数据Demo

    摘要:下滚动到页面底部无限加载数据看到一篇觉得挺实用的就看了下顺便简单翻译了一下给需要的人参考从这个项目中可以加深对的生命周期的理解何时开始请求如何结合使用原生来写事件等等我这里主要是对原文的重点提取和补充本文技术要点生命周期简单用法格式化日期图 Vue下滚动到页面底部无限加载数据Demo 看到一篇Implementing an Infinite Scroll with Vue.js, 觉得...

    elarity 评论0 收藏0

发表评论

0条评论

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