目錄
一、傳統的下拉加載方案
二、存在問題
1.性能較差
2.不夠精確
三、IntersectionObserve版本下拉加載
1、callback
2、options
四、IntersectionObserver實例
1、Intersection的優勢
2、實現思路
3、代碼實現
在進行前端開發的過程中,常常會碰到下拉加載列表數據的需求。本文將介紹如何利用Intersection API實現一個簡單的下拉加載數據的demo。
一、傳統的下拉加載方案
傳統的下拉加載方案大多數都是通過監聽scroll
事件,然后獲取目標元素坐標以及相關數據,再進行對應的實現。例如下面就是一個依賴數據列表容器的scrollHeight
、scrollTop
和height
實現的下拉加載的demo。
function App() {// 用于記錄當前是否正在請求中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) {// 當容器已經拉到最底部時,發起請求fetchData();}};containerRef.current.addEventListener('scroll', onScroll);return () => {containerRef.current.removeEventListener('scroll', onScroll);};}, []);const fetchData = () => {// 模擬數據請求// 如果當前正在請求中,直接返回if (loadingRef.current) return;// 標記當前正在請求中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>);
}
二、存在問題
1.性能較差
我們知道,scroll
事件的發生是十分密集的,在監聽scroll
事件的回調函數中,我們都要重新獲取列表容器的scrollTop
,這會導致“重排”的發生。此時需要我們額外去做一些防抖或是節流的工具,防止造成性能問題。
// 節流
throttle(onScroll, 500);
2.不夠精確
scrollTop
的小數問題 眼尖的同學可能已經看到的,我們在判斷容器是否已經滾動到底部是,還做了一個-1的操作。
if (scrollHeight - containerRef.current.scrollTop - 1 <= height) {// 當容器已經拉到最底部時,發起請求fetchData();
}
這是因為在使用顯示比例縮放的系統上,scrollTop可能會提供一個小數。如下圖所示,在容器滾動到底部時,scrollHeight(1542) - scrollTop(1141.5999755859375)
?與容器的高度height(400)
并不相等。
所以我們需要做出相應的兼容處理。
三、IntersectionObserve版本下拉加載
IntersectionObserver
提供了一種異步觀察目標元素在其祖先元素或頂級文檔視窗(viewport)中是否可視的方法。
IntersectionObserver的用法十分簡單,我們只需要定義好DOM元素的可視狀態發生變化后需要做些什么,以及需要觀察哪些元素的可視狀態就好了。
接下來我們詳細的看看intersectionObserver這個API。
const intersectionObserver = new IntersectionObserver(callback, options?) ;
?IntersectionObserver構造函數會接收兩個參數。
1、callback
callback為被觀察元素的可視狀態發生變更后的回調函數,此回調函數接受兩個參數:
function callback(entries, observer?) => {//...
}
entries
:一個IntersectionObserverEntry對象的數組。IntersectionObserverEntry對象用于描述被觀察對象的可視狀態的變化,擁有以下的屬性:
- entry.boundingClientRect:被觀察元素的邊界信息,相當于被觀察元素調用getBoundingClientRect()的結果。
- entry.intersectionRatio:被觀察元素與容器元素相交矩形面積與被觀察元素總面積的比例。
- entry.intersectionRect:相交矩形的邊界信息。
- entry.isIntersecting:一個布爾值,表示被觀察元素是否可視,如果是true,則表示元可視,反之則表示不可視。
- entry.rootBounds:容器元素的邊界信息,相當于容器元素調用getBoundingClientRect()的結果。
- entry.target:被觀察的元素的引用。
- entry.time:當前時間戳。
observer
:當前IntersectionObserver實例的引用。
2、options
options為一個可選參數,可傳入以下屬性:
- root:指定容器元素,默認為瀏覽器窗體元素。容器元素必須是目標元素的祖先節點。
- rootMargin:用于擴展或縮小rootBounds的大小,用法與CSS中margin一致,默認值為默認值是"0px 0px 0px 0px"。
- threshold:number或number數組,用于指定callback回調函數執行的閾值,如傳入
[0, 0.2, 0.6, 0.8, 1]
時,intersectionRatio每增加或減少0.2時都會觸發回調函數的執行。默認值為0
。需要注意的時,由于回調函數時異步觸發的,在回調函數執行時intersectionRatio可能已經和指定的閾值不一致了。
四、IntersectionObserver實例
IntersectionObserver構造函數會把options中的屬性掛載到IntersectionObserver實例上,并賦予IntersectionObserver實例四個方法:
- IntersectionObserver.disconnect():停止監聽工作。
- IntersectionObserver.observe(targetElem):開始監聽某個元素可視狀態的變化。
- IntersectionObserver.takeRecords():返回所有觀察目標的IntersectionObserverEntry對象數組。
- IntersectionObserver.unobserve(targetElem):停止監聽某個目標元素。
1、Intersection的優勢
intersectionObserver構造函數中傳入的回調函數只會在觀察的元素的可視狀態發生變化后才會執行,很好的解決傳統判斷可視的方案的性能瓶頸。
2、實現思路
我們在實現下拉加載功能時,當數據列表還沒有加載完時,我們往往會在數據列表的最后放置一個loading
組件,表示當數據列表還有更加數據,并且正在加載中。我們可以利用這個loading
組件的可視狀態以及Intersection
API實現Intersection版本的下拉加載。
3、代碼實現
實現一個DemoList.tsx
import { useEffect, useRef, useState } from 'react';const DemoList = () => {// 用于記錄當前是否正在請求中const loadingRef = useRef<boolean>(false);// loading divconst loadingDivRef = useRef<HTMLDivElement | null>(null);const observerRef = useRef<IntersectionObserver | null>(null);const [dataList, setDataList] = useState<number[]>([]);const fetchData = () => {// 模擬數據請求// 如果當前正在請求中,直接返回if (loadingRef.current) return;// 標記當前正在請求中loadingRef.current = true;setTimeout(() => {setDataList((_dataList) => {const dataList = [..._dataList];for (let i = 0; i < 100; i++) {// 這里面要注意的是把最新的請求的數據合并的時候要放在最后面,也就是說從數據的最后面添加。否則,就會出現連續請求的狀況。原因在于如果把最新的請求對的數據放最前面的話,新增的元素是從上面渲染,就會導致下面的加載元素一直處于可見的狀態,從而導致連續觸發的狀況dataList.push(Math.random());}return dataList;});loadingRef.current = false;}, 500);};useEffect(() => {fetchData();}, []);useEffect(() => {const target = loadingDivRef.current;if (!target) return;observerRef.current = new IntersectionObserver(function (entries) {if (entries[0].intersectionRatio > 0) {// intersectionRatio大于0,代表監聽的元素由不可見變成可見,進行數據請求fetchData();}});// 監聽Loading div的可見性if (loadingDivRef.current) observerRef.current.observe(loadingDivRef.current);return () => {if (observerRef.current) {if (target) observerRef.current.unobserve(target);observerRef.current.disconnect();observerRef.current = null;}};}, []);return (<div className="list-container">{dataList.map((item, index) => (<p className="list-item" key={item}>{index}——{item}</p>))}<div ref={loadingDivRef} className="loading">loading...</div></div>);
};export default DemoList;