阅读 481

移动端“加载更多”的原理和实现

背景

在长列表展示时,基于移动端的屏幕尺寸考虑,很少会有PC端的那种翻页交互设计,而且考虑到页面性能,也不可能一次加载完全部数据,取而代之的方案是在移动端的列表中,通过上拉加载更多数据。

loadmore.png

已经有很多组件库实现这个基础功能,比如 antd-mobile@2 中的 ListView ,但是一般配置项较多,使用起来较为繁琐。

原理

其实“加载更多”的原理也比较简单,首先获取列表的总长度和第一页的数据,如果第一页的数据长度小于列表的总长度,就在列表的底部增加“正在加载...”的元素,监听页面的滚动事件,如果底部的“正在加载...”元素出现在可视区域,我们就请求下一页的数据,并继续监听页面的滚动事件,直到当前列表展示数据等于列表的总长度。

所以主要的技术点是:怎么判断底部的“正在加载...”元素出现在了可视区域?

对于这个问题,大家可能首先想到的是监听页面滚动,然后动态计算底部的“正在加载...”元素位置。

监听页面滚动

移动端监听页面滚动很简单,以react为例:

useEffect(() => {
    function onTouchMove() {
			// do something...
    }

    document.addEventListener('touchmove', onTouchMove)

    return () => {
      document.removeEventListener('touchmove', onTouchMove)
    }
  }, [])
复制代码

PS:滚动的监听一般还需要考虑加上节流函数。

或者可以使用 requestAnimationFrame不停计算,取代对滚动的监听,对这个API不了解的同学,可以自行查阅MDN

const requestRef = useRef()
const scrollCb = () => {
    //  do something...    
    requestRef.current = requestAnimationFrame(scrollCb)
}
  
useEffect(() => {
    requestRef.current = requestAnimationFrame(scrollCb)
    return () => cancelAnimationFrame(requestRef.current)
}, [])
复制代码

可视区域检测

底部的“正在加载...”元素是否出现在可视区域,我们一般是有三种方案:

  • offsetTop、scrollTop

offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。

scrollTop:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0

function isInViewPortOfOne (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    return top <= viewPortHeight
}
复制代码
  • getBoundingClientRect

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。MDN

function isInViewPort(element) {
  const viewWidth = window.innerWidth || document.documentElement.clientWidth;
  const viewHeight = window.innerHeight || document.documentElement.clientHeight;
  const {
    top,
    right,
    bottom,
    left,
  } = element.getBoundingClientRect();

  return (
    top >= 0 &&
    left >= 0 &&
    right <= viewWidth &&
    bottom <= viewHeight
  );
}
复制代码
  • Intersection Observer

Intersection Observer 交叉观察器,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect会好很多。

它的用法非常简单。

var io = new IntersectionObserver(callback, option);
复制代码

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。

// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();
复制代码

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

io.observe(elementA);
io.observe(elementB);
复制代码

推荐方案和实现

通过上面的介绍,我们能得出结论:使用Intersection Observer是最简洁、性能最好的一种方案。

以 React.js 为例进行实现:

  // data 为当前展示的列表数据,total为列表的总数量
  // load-more 为底部“加载更多”元素的id
  // 处理加载更多
  useEffect(() => {
    if (data.length < total && document.querySelector('#load-more')) {
      const intersectionObserver = new IntersectionObserver(function (entries) {
        // 如果不可见,就返回
        if (entries[0].intersectionRatio <= 0) return
        
        // 触发加载更多
        triggerLoadMore?.()
      })
      // 开始观察
      intersectionObserver.observe(document.querySelector('#load-more'))

      return () => {
        intersectionObserver.disconnect()
      }
    }
  }, [data, total])
复制代码

大家可以发现,实现代码很简单,而且我们不需要再对滚动事件进行监听。

兼容性

jianrong.png

从上表可以看出,该API的兼容性已经很不错了。

w3c也提供了对该API的polyfill,见w3c/IntersectionObserver

日常开发中,推荐直接使用 polyfill.io 引入兼容代码:

<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
复制代码

其他优化点

  • 加入虚拟滚动

简单的理解,虚拟滚动就是在浏览器中,只渲染当前可视区域的内容,通过用户滑动滚动条的位置动态地来计算显示内容,其余部分用空白填充来给用户造成一个长列表的假象,可以解决长列表带来的页面滑动卡顿等问题。React.js 中可以使用react-window实现虚拟滚动。大家可以自行查阅资料,在此就不继续展开了。

Share

欢迎大家访问我们的小程序小程序前端面试题宝典,里面已经搜集了600+常见的前端面试题的题目和答案,希望能够帮助正在面试路上的同学。

也欢迎访问我们近期更新的文章:

前端社招神策数据二轮面经

明略前端三面面经(技术二面+HR面)

字节跳动商业化前端面试面经

美团四轮面试面经

爱奇艺前端二面面经

同时欢迎关注我们团队另一个掘金账号:

掘金前端面试题宝典

参考资料

文章分类
前端
文章标签