当前元素_90行代码,15个元素实现无限滚动
前言
在本篇文章你將會學到:
- IntersectionObserver API 的用法,以及如何兼容。
- 如何在React Hook中實現無限滾動。
- 如何正確渲染多達10000個元素的列表。
- 無限下拉加載技術使用戶在大量成塊的內容面前一直滾動查看。這種方法是在你向下滾動的時候不斷加載新內容。
當你使用滾動作為發現數據的主要方法時,它可能使你的用戶在網頁上停留更長時間并提升用戶參與度。隨著社交媒體的流行,大量的數據被用戶消費。無線滾動提供了一個高效的方法讓用戶瀏覽海量信息,而不必等待頁面的預加載。
如何構建一個體驗良好的無限滾動,是每個前端無論是項目或面試都會碰到的一個課題。
本文的原版實現來自:Creating Infinite Scroll with 15 Elements
1. 早期的解決方案
關于無限滾動,早期的解決方案基本都是依賴監聽scroll事件:
function fetchData() { fetch(path).then(res => doSomeThing(res.data)); } window.addEventListener('scroll', fetchData); 復制代碼
然后計算各種.scrollTop()、.offset().top等等。
手寫一個也是非常枯燥。而且:
- scroll事件會頻繁觸發,因此我們還需要手動節流。
- 滾動元素內有大量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('進入可視區域'); } }) }); intersectionObserver.observe(box); 復制代碼
敲重點: IntersectionObserver API是異步的,不隨著目標元素的滾動同步觸發,性能消耗極低。
2.1 IntersectionObserverEntry對象
這里我就粗略的介紹下需要用到的:IntersectionObserve初試IntersectionObserverEntry對象
callback函數被調用時,會傳給它一個數組,這個數組里的每個對象就是當前進入可視區域或者離開可視區域的對象(IntersectionObserverEntry對象)
這個對象有很多屬性,其中最常用的屬性是:
- target: 被觀察的目標元素,是一個 DOM 節點對象
- isIntersecting: 是否進入可視區域
- intersectionRatio: 相交區域和目標元素的比例值,進入可視區域,值大于0,否則等于0
2.3 options
調用IntersectionObserver時,除了傳一個回調函數,還可以傳入一個option對象,配置如下屬性:
- threshold: 決定了什么時候觸發回調函數。它是一個數組,每個成員都是一個門檻值,默認為[0],即交叉比例(intersectionRatio)達到0時觸發回調函數。用戶可以自定義這個數組。比如,[0, 0.25, 0.5, 0.75, 1]就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發回調函數。
- 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); //停止觀察nodeOne observer.disconnect(); //沒有觀察任何節點 復制代碼
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節點。那讓我們開始吧:
4. 原理
實現一個組件,可以顯示具有15個元素的固定窗口大小的n個項目的列表: 即在任何時候,無限滾動n元素上也僅存在15個DOM節點。
- 采用relative/absolute 定位來確定滾動位置
- 追蹤兩個ref: top/bottom來決定向上/向下滾動的渲染與否
- 切割數據列表,保留最多15個DOM元素。
5. useState聲明狀態變量
我們開始編寫組件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) } // 交叉觀察的具體回調,觀察每個節點,并對實時頭尾元素索引處理 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) } }); } // 停止滾動時放棄觀察 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; // 數據,節點高度 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循環中賦予頭尾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"> <h1>15個元素實現無限滾動</h1> <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萬下載量呢,放心用吧臭弟弟們。
總結
以上是生活随笔為你收集整理的当前元素_90行代码,15个元素实现无限滚动的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: swift 同步 网络请求_IOS开发中
- 下一篇: 网上申请信用卡技巧