溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

90行代碼,15個元素實現無限滾動

發(fā)布時間:2020-07-09 02:07:17 來源:網絡 閱讀:642 作者:可樂程序員 欄目:云計算

前言

在本篇文章你將會學到:

  • IntersectionObserver API 的用法,以及如何兼容。

  • 如何在React Hook中實現無限滾動。

  • 如何正確渲染多達10000個元素的列表。

90行代碼,15個元素實現無限滾動



  • 無限下拉加載技術使用戶在大量成塊的內容面前一直滾動查看。這種方法是在你向下滾動的時候不斷加載新內容。

當你使用滾動作為發(fā)現數據的主要方法時,它可能使你的用戶在網頁上停留更長時間并提升用戶參與度。隨著社交媒體的流行,大量的數據被用戶消費。無線滾動提供了一個高效的方法讓用戶瀏覽海量信息,而不必等待頁面的預加載。

90行代碼,15個元素實現無限滾動


如何構建一個體驗良好的無限滾動,是每個前端無論是項目或面試都會碰到的一個課題。

本文的原版實現來自: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,容易造成卡頓。

90行代碼,15個元素實現無限滾動


后來出現交叉觀察者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對象

90行代碼,15個元素實現無限滾動


這里我就粗略的介紹下需要用到的: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é)點。

90行代碼,15個元素實現無限滾動


  • 采用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%;
}
復制代碼

然后你就可以慢慢耍了。。。

90行代碼,15個元素實現無限滾動


10. 兼容性處理

IntersectionObserver不兼容Safari?

莫慌,我們有polyfill版

90行代碼,15個元素實現無限滾動


每周34萬下載量呢,放心用吧臭弟弟們。

90行代碼,15個元素實現無限滾動


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI