【性能优化-长列表渲染】React 中使用 Intersection Observer 和 LazyLoad来实现无限滚动,分页和懒加载

507 阅读4分钟

实现前需要了解这些

Intersection Observer API

根据MDN的文档,"Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档viewport的交集中的变化的方法。"。 通俗讲,就是一个能够监听元素是否到了当前视口的事件,一步到位

使用例子 对于实现一张图片的懒加载,我们可以采用ntersection Observer + dataSet。假如我们的电脑是1920*1080的话,现在有个图片,他距离顶部有1500px,我们希望在它在滚动到可视区域时加载

HTML
<img data-src="yuren.jpg" alt="" style="width: 300px;height: 300px;background-color: antiquewhite;margin-top: 1500px;">

JS 该方法接收一个回调函数,并且该函数的第一个参数是IntersectionObserverEntry接口的实例,实例描述了目标元素与其根元素容器在某一特定过渡时刻的交叉状态,我们使用该实例的一个属性intersectionRatio,该属性返回intersectionRect 与 boundingClientRect 的比例值, 判断该属性,为真则 IntersectionObserverEntry 描述了变换到交叉时的状态,这时我们替换data-src为src,完成懒加载功能。

const observer = new IntersectionObserver((changes) => {
  // changes: 目标元素集合
  changes.forEach((change) => {
    // intersectionRatio
    if (change.isIntersecting) {
      const img = change.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

兼容性 兼容性目前享有被大多数浏览器支持(ie bushi)

image.png

LazyLoad插件

import React from 'react';
import ReactDOM from 'react-dom';
import LazyLoad from 'react-lazyload';
import MyComponent from './MyComponent';
const App = () => {
  return (
    <div className="list">
      <LazyLoad height={200}>
        <img src="tiger.jpg" /> 
        /*开箱即用支持延迟加载图像,无需额外配置,基本使用的话设置“height”即可 */
      </LazyLoad>
      <LazyLoad height={200} once >
     /* once属性指的是一旦该元素进入可视区域范围,元素DOM被加载,
     LazyLoad之后就不会再将它重新回缩,但在后面项目我们不设置这种参数 */
        <MyComponent />
      </LazyLoad>
      <LazyLoad height={200} offset={100}>
     /* 此组件将在顶部时加载边缘距离视口为100px。这会让用户无法感知到懒加载 */
        <MyComponent />
      </LazyLoad>
      <LazyLoad>
        <MyComponent />
      </LazyLoad>
    </div>
  );
};

ReactDOM.render(<App />, document.body);

更多props参数可以去 github地址

具体实现

解决的痛点:由于异常数据有时候会有几w条,因此需要滚动分页和懒加载来进行优化

useEffect Hooks

首先,我们定义一个reducer函数--pageReducer。这个Reducer处理两个Action。

  1. NEXT_PAGE Action 将page分页+1,接口将发送分页请求。
  2. FIRST_PAGE Action 设置page为1。

下一步是将这个reducer连接到useReducer 钩子函数上。一旦完成该步,我们就会得到pager(是分页数据), pagerDispatch(更新reducer对象的函数)。

function App() {
 function pageReducer(state: any, action: { type: string }) { 
  switch (action.type) { 
   case 'NEXT_PAGE': 
    return { ...state, page: state.page + 1 }; 
   case 'FIRST_PAGE': { 
    return { ...state, page: 1 }; } 
   default: return state; 
   } 
  }
  const [pager, pagerDispatch] = useReducer(pageReducer, { page: 1, limit: 16 });
  //...
}

IntersectionObserver API使用

接下来我们使用IntersectionObserver进行监听,我们定义一个变量bottomBoundaryRef,并将其值设置为useRef(null)useRef可以让变量在整个组件重新渲染时保留其值,也就是说,当包含的组件重新渲染时,变量的当前值会持续存在。改变其值的唯一方法是重新分配该变量的.current属性。

在我们的例子中,bottomBoundaryRef.current的起始值为null。随着页面不断的滚动,当我们的页面到底时,将其当前属性设置为节点<div id='page-bottom-border'>

代码如下

let bottomBoundaryRef = useRef(null); 
const useInfiniteScroll = (scrollRef: any, dispatch: any) => { 
 const scrollObserver = useCallback( node => { 
  new IntersectionObserver(entries => { 
  entries.forEach(en => { 
   if (en.intersectionRatio > 0) { 
   dispatch({ type: 'NEXT_PAGE'  }); 
  } 
 }); 
 }).observe(node); 
 }, [dispatch] ); 
 useEffect(() => { 
  if (scrollRef.current) { 
   scrollObserver(scrollRef.current); 
  } 
 }, [scrollObserver, scrollRef]);

而在渲染时,根据分辨率划分3个或者4个card

LazyLoad设置scrollContainer为滚动区域荣容器,height为DOM高度

 {
 dataSource.map((j: any, index: any) => { 
    return ( 
    <LazyLoad offset={420} 
    style={{ 
        display: 'flex', 
        flexWrap: 'wrap', 
        marginLeft: index % colNum === 0 ? 0 : '16px', 
        width: window.innerWidth > 1440 ? 'calc((100% - 48px) / 4)' : 'calc((100% - 32px) / 3)' 
        }} 
    height={405} 
    key={`lazy${j.id}`} 
    scroll={true} 
    scrollContainer=".container" > 
...
     </LazyLoad> ); }
     )} 
 <div id="page-bottom-boundary" ref={bottomBoundaryRef} /> 
 {loading && <Loading />
 }

整个功能的流程总结如下

与观测区域产生交集 ==> 派发NEXT_PAGEaction ==> 将pager.page的值增量1 ==> useEffect钩子函数执行axios请求分页调用 ==> axios请求分页调用执行 ==> 返回的数据被concat到原数据列表中。

这里用到concat而没有用到es6+的...语法,因为需要频繁触发该操作,做了concat...的对比实验,发现concat性能好。

结论

H5不断的新增的API,便利着我们实现更多功能,以前实现懒加载,可能需要 window.scroll 监听 Element.getBoundingClientRect() 并使用 _.throttle 节流。**

这一套组合拳太复杂了,于是浏览器出了一个三合一事件: IntersectionObserver API,一个能够监听元素是否到了当前视口的事件,一步到位!

~希望能对你有用,有哪里不对的也请大佬们指点,喜欢的可以点赞支持 ~