责编 | 胡巍巍
在本篇文章你将会学到:
Intersectionobserver API的用法,以及如何兼容。
如何在React Hook中实现无限滚动。
如何精确渲染多达10000个元素的列表。
无限下拉加载技能利用户在大量成块的内容面前一贯滚动查看。这种方法是在你向下滚动的时候不断加载新内容。
当你利用滚动作为创造数据的紧张方法时,它可能使你的用户在网页上勾留更永劫光并提升用户参与度。
随着社交媒体的盛行,大量的数据被用户消费。无线滚动供应了一个高效的方法让用户浏览海量信息,而不必等待页面的预加载。
如何构建一个体验良好的无限滚动,是每个前端无论是项目或口试都会碰到的一个课题。
早期的办理方案
关于无限滚动,早期的办理方案基本都是依赖监听scroll事宜:
function fetchData {fetch(path).then(res => doSomeThing(res.data));}window.addEventListener('scroll', fetchData);
然后打算各种.scrollTop、.offset.top等等。
手写一个也是非常呆板。而且:
scroll事宜会频繁触发,因此我们还须要手动节流。
滚动元素内有大量DOM,随意马虎造成卡顿。
交叉不雅观察者: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工具这里我就粗略的先容下须要用到的: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和nodeTwoobserver.unobserve(nodeOne); //停滞不雅观察nodeOneobserver.disconnect; //没有不雅观察任何节点
如何在React Hook中利用IntersectionObserver在看Hooks版之前,来看正常组件版的:
class SlidingWindowScroll extends React.Component {this.$bottomElement = React.createRef;...componentDidMount {this.intiateScrollObserver;}intiateScrollObserver = => {const options = {root: ,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节点。那让我们开始吧:
事理
实现一个组件,可以显示具有15个元素的固定窗口大小的n个项目的列表:
即在任何时候,无限滚动n元素上也仅存在15个DOM节点。
采取relative/absolute 定位来确定滚动位置
追踪两个ref: top/bottom来决定向上/向下滚动的渲染与否
切割数据列表,保留最多15个DOM元素。
useState 声明状态变量
我们开始编写组件SlidingWindowScrollHook:
const THRESHOLD = 15;const SlidingWindowScrollHook = (props) => {const [start, setStart] = useState(0);const [end, setEnd] = useState(THRESHOLD);const [observer, setObserver] = useState;// 其它代码...}
1. useState 的大略理解:
const [属性, 操作属性的方法] = useState(默认值);
2. 变量解析start:当前渲染的列表第一个数据,默认为0
end: 当前渲染的列表末了一个数据,默认为15
observer: 当前不雅观察的视图ref元素
useRef 定义追踪的DOM 元素
const $bottomElement = useRef;const $topElement = useRef;
正常的无限向下滚动只需关注一个dom元素,但由于我们是固定15个dom元素渲染,
须要判断向上或向下滚动。内部操作方法和和对应useEffect请合营注释食用:
useEffect( => {// 定义不雅观察intiateScrollObserver;return => {// 放弃不雅观察resetObservation}},[end]) //由于[end] 是同步刷新,这里用一个就行了。// 定义不雅观察const intiateScrollObserver = => {const options = {root: ,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); // 向上滚动尾部元素索引不得小于15let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0); // 头部元素索引最小值为0setStart(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 ;}
渲染界面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循环中授予头尾refconst id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : ''); // 绑IDreturn (<li className=\公众li-card\"大众 key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);})}</ul>);
如何利用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;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: ,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 Downif (entry.isIntersecting && entry.target.id === \公众bottom\"大众) {const maxStartIndex = listLength - 1 - THRESHOLD; // Maximum index value `start` can takeconst maxEndIndex = listLength - 1; // Maximum index value `end` can takeconst newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex;const newStart = (end - 5) <= maxStartIndex ? end - 5 : maxStartIndex;setStart(newStart)setEnd(newEnd)}// Scroll upif (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 ;}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%;}
然后你就可以逐步耍了。
兼容性处理IntersectionObserver不兼容Safari?
莫慌。我们有polyfill版。
每周34万下载量呢,放心用吧。
项目源地址:https://github.com/roger-hiro/SlidingWindowScrollHook
资料:
Creating Infinite Scroll with 15 Elements
IntersectionObserve初试
【END】