利用Intersection實(shí)現(xiàn)下拉加載數(shù)據(jù)

在我們進(jìn)行業(yè)務(wù)開發(fā)的過程中,常常會(huì)碰到下拉加載列表數(shù)據(jù)的需求。本文將介紹如何利用Intersection API實(shí)現(xiàn)一個(gè)簡單的下拉加載數(shù)據(jù)的demo。

傳統(tǒng)的下拉加載方案

傳統(tǒng)的下拉加載方案大多數(shù)都是通過監(jiān)聽scroll事件,然后獲取目標(biāo)元素坐標(biāo)以及相關(guān)數(shù)據(jù),再進(jìn)行對(duì)應(yīng)的實(shí)現(xiàn)。例如下面就是一個(gè)依賴數(shù)據(jù)列表容器的scrollHeight、scrollTopheight實(shí)現(xiàn)的下拉加載的demo。

代碼實(shí)現(xiàn)

function App() {
  // 用于記錄當(dāng)前是否正在請(qǐng)求中
  const loadingRef = useRef<boolean>(false);
  // 列表容器
  const containerRef = useRef<HTMLDivElement>(null);
  const [dataList, setDataList] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  useEffect(() => {
    const { height } = containerRef.current.getBoundingClientRect();
    const scrollHeight = containerRef.current.scrollHeight;

    const onScroll = () => {
      console.log('scrollHeight:', scrollHeight, 'scrollTop:', containerRef.current.scrollTop, 'height:', height);
      if (scrollHeight - containerRef.current.scrollTop - 1 <= height) {
        // 當(dāng)容器已經(jīng)拉到最底部時(shí),發(fā)起請(qǐng)求
        fetchData();
      }
    };

    containerRef.current.addEventListener('scroll', onScroll);

    return () => {
      containerRef.current.removeEventListener('scroll', onScroll);
    };
  }, []);

  const fetchData = () => {
    // 模擬數(shù)據(jù)請(qǐng)求
    // 如果當(dāng)前正在請(qǐng)求中,直接返回
    if (loadingRef.current) return;
    // 標(biāo)記當(dāng)前正在請(qǐng)求中
    loadingRef.current = true;
    setTimeout(() => {
      setDataList(_dataList => {
        const dataList = [..._dataList];
        for (let i = 0; i < 20; i++) {
          dataList.push(Math.random());
        }

        return dataList;
      });
      loadingRef.current = false;
    }, 500);
  };

  return (
    <div ref={containerRef} className="list-container">
      {dataList.map(item => (
        <p className="list-item" key={item}>
          {item}
        </p>
      ))}
      <div className="loading">loading...</div>
    </div>
  );
}

實(shí)現(xiàn)效果

2021-12-26 17-06-14 00_00_02-00_00_07.gif

存在問題

1.性能較差
我們知道,scroll事件的發(fā)生是十分密集的,在監(jiān)聽scroll事件的回調(diào)函數(shù)中,我們都要重新獲取列表容器的scrollTop這會(huì)導(dǎo)致“重排”的發(fā)生。此時(shí)需要我們額外去做一些防抖或是節(jié)流的工具,防止造成性能問題。

// 節(jié)流
throttle(onScroll, 500);

2.scrollTop的小數(shù)問題
眼尖的同學(xué)可能已經(jīng)看到的,我們在判斷容器是否已經(jīng)滾動(dòng)到底部是,還做了一個(gè)-1的操作。

if (scrollHeight - containerRef.current.scrollTop - 1 <= height) {
  // 當(dāng)容器已經(jīng)拉到最底部時(shí),發(fā)起請(qǐng)求
  fetchData();
}

這是因?yàn)樵谑褂蔑@示比例縮放的系統(tǒng)上,scrollTop可能會(huì)提供一個(gè)小數(shù)。如下圖所示,在容器滾動(dòng)到底部時(shí),scrollHeight(1542) - scrollTop(1141.5999755859375) 與容器的高度height(400)并不相等。

image.png

所以我們需要做出相應(yīng)的兼容處理。

Intersection版本下拉加載

Intersection

IntersectionObserver 提供了一種異步觀察目標(biāo)元素在其祖先元素或頂級(jí)文檔視窗(viewport)中是否可視的方法。

IntersectionObserver的用法十分簡單,我們只需要定義好DOM元素的可視狀態(tài)發(fā)生變化后需要做些什么,以及需要觀察哪些元素的可視狀態(tài)就好了。

接下來我們詳細(xì)的看看intersectionObserver這個(gè)API。

const intersectionObserver = new IntersectionObserver(callback, options?) ;

IntersectionObserver構(gòu)造函數(shù)會(huì)接收兩個(gè)參數(shù)。

callback

callback為被觀察元素的可視狀態(tài)發(fā)生變更后的回調(diào)函數(shù),此回調(diào)函數(shù)接受兩個(gè)參數(shù):

function callback(entries, observer?) => {
  //...
}

entries:一個(gè)IntersectionObserverEntry對(duì)象的數(shù)組。IntersectionObserverEntry對(duì)象用于描述被觀察對(duì)象的可視狀態(tài)的變化,擁有以下的屬性:

  • entry.boundingClientRect:被觀察元素的邊界信息,相當(dāng)于被觀察元素調(diào)用getBoundingClientRect()的結(jié)果。
  • entry.intersectionRatio:被觀察元素與容器元素相交矩形面積與被觀察元素總面積的比例。
  • entry.intersectionRect:相交矩形的邊界信息。
  • entry.isIntersecting:一個(gè)布爾值,表示被觀察元素是否可視,如果是true,則表示元可視,反之則表示不可視。
  • entry.rootBounds:容器元素的邊界信息,相當(dāng)于容器元素調(diào)用getBoundingClientRect()的結(jié)果。
  • entry.target:被觀察的元素的引用。
  • entry.time:當(dāng)前時(shí)間戳。

observer:當(dāng)前IntersectionObserver實(shí)例的引用。

options

options為一個(gè)可選參數(shù),可傳入以下屬性:

  • root:指定容器元素,默認(rèn)為瀏覽器窗體元素。容器元素必須是目標(biāo)元素的祖先節(jié)點(diǎn)。
  • rootMargin:用于擴(kuò)展或縮小rootBounds的大小,用法與CSS中margin一致,默認(rèn)值為默認(rèn)值是"0px 0px 0px 0px"。
  • threshold:number或number數(shù)組,用于指定callback回調(diào)函數(shù)執(zhí)行的閾值,如傳入[0, 0.2, 0.6, 0.8, 1]時(shí),intersectionRatio每增加或減少0.2時(shí)都會(huì)觸發(fā)回調(diào)函數(shù)的執(zhí)行。默認(rèn)值為0。需要注意的時(shí),由于回調(diào)函數(shù)時(shí)異步觸發(fā)的,在回調(diào)函數(shù)執(zhí)行時(shí)intersectionRatio可能已經(jīng)和指定的閾值不一致了。

IntersectionObserver實(shí)例

IntersectionObserver構(gòu)造函數(shù)會(huì)把options中的屬性掛載到IntersectionObserver實(shí)例上,并賦予IntersectionObserver實(shí)例四個(gè)方法:

  • IntersectionObserver.disconnect():停止監(jiān)聽工作。
  • IntersectionObserver.observe(targetElem):開始監(jiān)聽某個(gè)元素可視狀態(tài)的變化。
  • IntersectionObserver.takeRecords():返回所有觀察目標(biāo)的IntersectionObserverEntry對(duì)象數(shù)組。
  • IntersectionObserver.unobserve(targetElem):停止監(jiān)聽某個(gè)目標(biāo)元素。

Intersection的優(yōu)勢

intersectionObserver構(gòu)造函數(shù)中傳入的回調(diào)函數(shù)只會(huì)在觀察的元素的可視狀態(tài)發(fā)生變化后才會(huì)執(zhí)行,很好的解決傳統(tǒng)判斷可視的方案的性能瓶頸。

實(shí)現(xiàn)思路

我們在實(shí)現(xiàn)下拉加載功能時(shí),當(dāng)數(shù)據(jù)列表還沒有加載完時(shí),我們往往會(huì)在數(shù)據(jù)列表的最后放置一個(gè)loading組件,表示當(dāng)數(shù)據(jù)列表還有更加數(shù)據(jù),并且正在加載中。我們可以利用這個(gè)loading組件的可視狀態(tài)以及Intersection API實(shí)現(xiàn)Intersection版本的下拉加載。

代碼實(shí)現(xiàn)

function App() {
  // 用于記錄當(dāng)前是否正在請(qǐng)求中
  const loadingRef = useRef<boolean>(false);
  // loading div
  const loadingDivRef = useRef<HTMLDivElement>(null);
  const [dataList, setDataList] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表監(jiān)聽的元素由不可見變成可見,進(jìn)行數(shù)據(jù)請(qǐng)求
        fetchData();
      }
    });

    // 監(jiān)聽Loading div的可見性
    intersectionObserver.observe(loadingDivRef.current);

    return () => {
      intersectionObserver.unobserve(loadingDivRef.current);
      intersectionObserver.disconnect();
      intersectionObserver = null;
    };
  }, []);

  const fetchData = () => {
    // 模擬數(shù)據(jù)請(qǐng)求
    // 如果當(dāng)前正在請(qǐng)求中,直接返回
    if (loadingRef.current) return;
    // 標(biāo)記當(dāng)前正在請(qǐng)求中
    loadingRef.current = true;
    setTimeout(() => {
      setDataList(_dataList => {
        const dataList = [..._dataList];
        for (let i = 0; i < 20; i++) {
          dataList.push(Math.random());
        }

        return dataList;
      });
      loadingRef.current = false;
    }, 500);
  };

  return (
    <div className="list-container">
      {dataList.map(item => (
        <p className="list-item" key={item}>
          {item}
        </p>
      ))}
      <div ref={loadingDivRef} className="loading">
        loading...
      </div>
    </div>
  );
}

實(shí)現(xiàn)效果

2021-12-26 15-13-22 00_00_03-00_00_06.gif
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容