您好,登錄后才能下訂單哦!
前言
在本篇文章你將會學到:
IntersectionObserver API 的用法,以及如何兼容。
如何在React Hook中實現無限滾動。
如何正確渲染多達10000個元素的列表。
無限下拉加載技術使用戶在大量成塊的內容面前一直滾動查看。這種方法是在你向下滾動的時候不斷加載新內容。
當你使用滾動作為發(fā)現數據的主要方法時,它可能使你的用戶在網頁上停留更長時間并提升用戶參與度。隨著社交媒體的流行,大量的數據被用戶消費。無線滾動提供了一個高效的方法讓用戶瀏覽海量信息,而不必等待頁面的預加載。
如何構建一個體驗良好的無限滾動,是每個前端無論是項目或面試都會碰到的一個課題。
本文的原版實現來自:Creating Infinite Scroll with 15 Elements
1. 早期的解決方案
關于無限滾動,早期的解決方案基本都是依賴監(jiān)聽scroll事件:
function?fetchData()?{ ?fetch(path).then(res?=>?doSomeThing(res.data)); }window.addEventListener('scroll',?fetchData); 復制代碼
然后計算各種.scrollTop()、.offset().top等等。
手寫一個也是非??菰?。而且:
scroll事件會頻繁觸發(fā),因此我們還需要手動節(jié)流。
滾動元素內有大量DOM,容易造成卡頓。
后來出現交叉觀察者IntersectionObserver API ,在與Vue、React這類數據驅動視圖的框架后,無限滾動的通用方案就出來了。2. 交叉觀察者:IntersectionObserver
const?box?=?document.querySelector('.box'); const?intersectionObserver?=?new?IntersectionObserver((entries)?=>?{ ?entries.forEach((item)?=>?{?if?(item.isIntersecting)?{?console.log('進入可視區(qū)域'); ?} ?}) }); intersectionObserver.observe(box); 復制代碼
敲重點:?IntersectionObserver API是異步的,不隨著目標元素的滾動同步觸發(fā),性能消耗極低。
2.1 IntersectionObserverEntry對象
這里我就粗略的介紹下需要用到的:IntersectionObserve初試
IntersectionObserverEntry對象
callback函數被調用時,會傳給它一個數組,這個數組里的每個對象就是當前進入可視區(qū)域或者離開可視區(qū)域的對象(IntersectionObserverEntry對象)
這個對象有很多屬性,其中最常用的屬性是:
target: 被觀察的目標元素,是一個 DOM 節(jié)點對象
isIntersecting: 是否進入可視區(qū)域
intersectionRatio: 相交區(qū)域和目標元素的比例值,進入可視區(qū)域,值大于0,否則等于0
2.3 options
調用IntersectionObserver時,除了傳一個回調函數,還可以傳入一個option對象,配置如下屬性:
threshold: 決定了什么時候觸發(fā)回調函數。它是一個數組,每個成員都是一個門檻值,默認為[0],即交叉比例(intersectionRatio)達到0時觸發(fā)回調函數。用戶可以自定義這個數組。比如,[0, 0.25, 0.5, 0.75, 1]就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發(fā)回調函數。
root: 用于觀察的根元素,默認是瀏覽器的視口,也可以指定具體元素,指定元素的時候用于觀察的元素必須是指定元素的子元素
rootMargin: 用來擴大或者縮小視窗的的大小,使用css的定義方法,10px 10px 30px 20px表示top、right、bottom 和 left的值
const?io?=?new?IntersectionObserver((entries)?=>?{?console.log(entries); },?{ ?threshold:?[0,?0.5], ?root:?document.querySelector('.container'), ?rootMargin:?"10px?10px?30px?20px", }); 復制代碼
2.4 observer
observer.observer(nodeone);?//僅觀察nodeOne?observer.observer(nodeTwo);?//觀察nodeOne和nodeTwo?observer.unobserve(nodeOne);?//停止觀察nodeOneobserver.disconnect();?//沒有觀察任何節(jié)點復制代碼
3. 如何在React Hook中使用IntersectionObserver
在看Hooks版之前,來看正常組件版的:
class?SlidingWindowScroll?extends?React.Component?{this.$bottomElement?=?React.createRef(); ... componentDidMount()?{?this.intiateScrollObserver(); } intiateScrollObserver?=?()?=>?{ ?const?options?=?{ ?root:?null, ?rootMargin:?'0px', ?threshold:?0.1 ?};?this.observer?=?new?IntersectionObserver(this.callback,?options);?this.observer.observe(this.$bottomElement.current); } render()?{?return?( ?<li?className='img'?ref={this.$bottomElement}> ?) } 復制代碼
眾所周知,React 16.x后推出了useRef來替代原有的createRef,用于追蹤DOM節(jié)點。那讓我們開始吧:
4. 原理
實現一個組件,可以顯示具有15個元素的固定窗口大小的n個項目的列表: 即在任何時候,無限滾動n元素上也僅存在15個DOM節(jié)點。
采用relative/absolute 定位來確定滾動位置
追蹤兩個ref: top/bottom來決定向上/向下滾動的渲染與否
切割數據列表,保留最多15個DOM元素。
5. useState聲明狀態(tài)變量
我們開始編寫組件SlidingWindowScrollHook:
const?THRESHOLD?=?15;const?SlidingWindowScrollHook?=?(props)?=>?{?const?[start,?setStart]?=?useState(0);?const?[end,?setEnd]?=?useState(THRESHOLD);?const?[observer,?setObserver]?=?useState(null);?//?其它代碼...} 復制代碼
1. useState的簡單理解:
const?[屬性,?操作屬性的方法]?=?useState(默認值); 復制代碼
2. 變量解析
start:當前渲染的列表第一個數據,默認為0
end: 當前渲染的列表最后一個數據,默認為15
observer: 當前觀察的視圖ref元素
6. useRef定義追蹤的DOM元素
const?$bottomElement?=?useRef();const?$topElement?=?useRef();復制代碼
正常的無限向下滾動只需關注一個dom元素,但由于我們是固定15個dom元素渲染,需要判斷向上或向下滾動。
7. 內部操作方法和和對應useEffect
請配合注釋食用:
useEffect(()?=>?{?//?定義觀察 ?intiateScrollObserver();?return?()?=>?{?//?放棄觀察 ?resetObservation() ?} },[end])?//因為[end]?是同步刷新,這里用一個就行了。//?定義觀察const?intiateScrollObserver?=?()?=>?{?const?options?=?{ ?root:?null, ?rootMargin:?'0px', ?threshold:?0.1 ?};?const?Observer?=?new?IntersectionObserver(callback,?options)?//?分別觀察開頭和結尾的元素 ?if?($topElement.current)?{ ?Observer.observe($topElement.current); ?}?if?($bottomElement.current)?{ ?Observer.observe($bottomElement.current); ?}?//?設初始值 ?setObserver(Observer)? }//?交叉觀察的具體回調,觀察每個節(jié)點,并對實時頭尾元素索引處理const?callback?=?(entries,?observer)?=>?{ ?entries.forEach((entry,?index)?=>?{?const?listLength?=?props.list.length;?//?向下滾動,刷新數據 ?if?(entry.isIntersecting?&&?entry.target.id?===?"bottom")?{?const?maxStartIndex?=?listLength?-?1?-?THRESHOLD;?//?當前頭部的索引 ?const?maxEndIndex?=?listLength?-?1;?//?當前尾部的索引 ?const?newEnd?=?(end?+?10)?<=?maxEndIndex???end?+?10?:?maxEndIndex;?//?下一輪增加尾部 ?const?newStart?=?(end?-?5)?<=?maxStartIndex???end?-?5?:?maxStartIndex;?//?在上一輪的基礎上計算頭部 ?setStart(newStart) ?setEnd(newEnd) ?}?//?向上滾動,刷新數據 ?if?(entry.isIntersecting?&&?entry.target.id?===?"top")?{?const?newEnd?=?end?===?THRESHOLD???THRESHOLD?:?(end?-?10?>?THRESHOLD???end?-?10?:?THRESHOLD);?//?向上滾動尾部元素索引不得小于15 ?let?newStart?=?start?===?0???0?:?(start?-?10?>?0???start?-?10?:?0);?//?頭部元素索引最小值為0 ?setStart(newStart) ?setEnd(newEnd) ?} ?}); }//?停止?jié)L動時放棄觀察const?resetObservation?=?()?=>?{ ?observer?&&?observer.unobserve($bottomElement.current);? ?observer?&&?observer.unobserve($topElement.current); }//?渲染時,頭尾ref處理const?getReference?=?(index,?isLastIndex)?=>?{?if?(index?===?0)?return?$topElement;?if?(isLastIndex)? ?return?$bottomElement;?return?null; } 復制代碼
8. 渲染界面
?const?{list,?height}?=?props;?//?數據,節(jié)點高度 ?const?updatedList?=?list.slice(start,?end);?//?數據切割 ? ?const?lastIndex?=?updatedList.length?-?1;?return?( ?<ul?style={{position:?'relative'}}> ?{updatedList.map((item,?index)?=>?{?const?top?=?(height?*?(index?+?start))?+?'px';?//?基于相對?&?絕對定位?計算 ?const?refVal?=?getReference(index,?index?===?lastIndex);?//?map循環(huán)中賦予頭尾ref ?const?id?=?index?===?0???'top'?:?(index?===?lastIndex???'bottom'?:?'');?//?綁ID ?return?(<li?className="li-card"?key={item.key}?style={{top}}?ref={refVal}?id={id}>{item.value}</li>); ?})} ?</ul> ?); 復制代碼
9. 如何使用
App.js:
import?React?from?'react';import?'./App.css';import?{?SlidingWindowScrollHook?}?from?"./SlidingWindowScrollHook";import?MY_ENDLESS_LIST?from?'./Constants';function?App()?{?return?(?<div?className="App"> ?<h2>15個元素實現無限滾動</h2> ?<SlidingWindowScrollHook?list={MY_ENDLESS_LIST}?height={195}/> ?</div> ?); } export?default?App; 復制代碼
定義一下數據 Constants.js:
const?MY_ENDLESS_LIST?=?[ ?{ ?key:?1,?value:?'A' ?}, ?{ ?key:?2,?value:?'B' ?}, ?{ ?key:?3,?value:?'C' ?},?//?中間就不貼了... ?{ ?key:?45,?value:?'AS' ?} ] 復制代碼
SlidingWindowScrollHook.js:
import?React,?{?useState,?useEffect,?useRef?}?from?"react";const?THRESHOLD?=?15;const?SlidingWindowScrollHook?=?(props)?=>?{?const?[start,?setStart]?=?useState(0);?const?[end,?setEnd]?=?useState(THRESHOLD);?const?[observer,?setObserver]?=?useState(null);?const?$bottomElement?=?useRef();?const?$topElement?=?useRef(); ?useEffect(()?=>?{ ?intiateScrollObserver();?return?()?=>?{ ?resetObservation() ?}?//?eslint-disable-next-line?react-hooks/exhaustive-deps ?},[start,?end])?const?intiateScrollObserver?=?()?=>?{?const?options?=?{ ?root:?null, ?rootMargin:?'0px', ?threshold:?0.1 ?};?const?Observer?=?new?IntersectionObserver(callback,?options)?if?($topElement.current)?{ ?Observer.observe($topElement.current); ?}?if?($bottomElement.current)?{ ?Observer.observe($bottomElement.current); ?} ?setObserver(Observer)? ?}?const?callback?=?(entries,?observer)?=>?{ ?entries.forEach((entry,?index)?=>?{?const?listLength?=?props.list.length;?//?Scroll?Down ?if?(entry.isIntersecting?&&?entry.target.id?===?"bottom")?{?const?maxStartIndex?=?listLength?-?1?-?THRESHOLD;?//?Maximum?index?value?`start`?can?take ?const?maxEndIndex?=?listLength?-?1;?//?Maximum?index?value?`end`?can?take ?const?newEnd?=?(end?+?10)?<=?maxEndIndex???end?+?10?:?maxEndIndex;?const?newStart?=?(end?-?5)?<=?maxStartIndex???end?-?5?:?maxStartIndex; ?setStart(newStart) ?setEnd(newEnd) ?}?//?Scroll?up ?if?(entry.isIntersecting?&&?entry.target.id?===?"top")?{?const?newEnd?=?end?===?THRESHOLD???THRESHOLD?:?(end?-?10?>?THRESHOLD???end?-?10?:?THRESHOLD); ?let?newStart?=?start?===?0???0?:?(start?-?10?>?0???start?-?10?:?0); ?setStart(newStart) ?setEnd(newEnd) ?} ? ?}); ?}?const?resetObservation?=?()?=>?{ ?observer?&&?observer.unobserve($bottomElement.current); ?observer?&&?observer.unobserve($topElement.current); ?}?const?getReference?=?(index,?isLastIndex)?=>?{?if?(index?===?0)?return?$topElement;?if?(isLastIndex)? ?return?$bottomElement;?return?null; ?}?const?{list,?height}?=?props;?const?updatedList?=?list.slice(start,?end);?const?lastIndex?=?updatedList.length?-?1;? ?return?( ?<ul?style={{position:?'relative'}}> ?{updatedList.map((item,?index)?=>?{?const?top?=?(height?*?(index?+?start))?+?'px';?const?refVal?=?getReference(index,?index?===?lastIndex);?const?id?=?index?===?0???'top'?:?(index?===?lastIndex???'bottom'?:?'');?return?(<li?className="li-card"?key={item.key}?style={{top}}?ref={refVal}?id={id}>{item.value}</li>); ?})} ?</ul> ?); } export?{?SlidingWindowScrollHook?}; 復制代碼
以及少許樣式:
.li-card?{?display:?flex;?justify-content:?center;?list-style:?none;?box-shadow:?2px?2px?9px?0px?#bbb;?padding:?70px?0;?margin-bottom:?20px;?border-radius:?10px;?position:?absolute;?width:?80%; } 復制代碼
然后你就可以慢慢耍了。。。
10. 兼容性處理
IntersectionObserver不兼容Safari?
莫慌,我們有polyfill版
每周34萬下載量呢,放心用吧臭弟弟們。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。