前言
先看效果
话不多说直接上链接 我已将项目托管到gitpage 点我在线体验
组件源码地址 源码地址
使用步骤
- 下载源码压缩包并解压
- 将对应文件添加到相应目录下
- 给
citylist组件配置一个路由 当你点击你所写的选择城市时跳转到这个路由即可 - 因为我把每一个城市列表项写成了
Link所以当你点击你想选择的城市后,将会跳转到to属性的路由下,所以你需要将VirtualList组件中的Link的to属性修改为你使用这个城市选择组件的路由路径 用const [search]=useSearchParams() const cityName=search.get('name')||''获取到你所选择的城市,再把你写的城市修改为你获取到的城市即可实现城市切换功能啦
设计思路
- 先封装一个虚拟列表组件(其实也没有很大必要,因为城市列表才几百条数据,长列表在数据量以万计时才能真正展现它的性能优势,数据量越大,优势越明显)但是我还是硬着头皮写了,也讲一下它的原理吧,长列表在性能优化这一块还是经常会遇见的。
- 虚拟列表组件封装好了之后,给它传要渲染的城市列表数据。
- 再封装一个右侧字母索引的列表,以便用户点击字母能快速跳到相应的字母城市列表下。
- 接下来就是实现两个列表的联动了,城市列表在滚动的同时,右侧字母索引列表对应的字母要高亮 ,用户点击右侧列表的字母,城市列表要能跳到相应字母的位置。
- 最后就是给使用这个城市选择组件路由带回去你所选择的城市的名字了,这样就可以实现切换城市的效果了。
主要知识点
虚拟列表
虚拟列表的核心就是优先渲染可视区和即将进入可视区元素,而不是一次性加载全部。
那么如何判断一个元素是否在可是区域内?
这个知识点其实不仅仅只在虚拟列表中要用到,还有比如图片懒加载也要用到,所以还是很实用的,希望大家读后能有所收获
方法一 利用offsetTop和scrollTop
判断
offsetTop-scrollTop的值是否小于等于可视窗区域的高度,如果是则说明元素在可视区内。
方法二 利用getBoundingClientRect
getBoundingClientRect()返回值是一个 DOMRect对象,拥有left, top, right, bottom, x, y, width, 和 height属性
当页面发生滚动的时候,属性的值会跟着变化
那么如果一个元素在可视区的话,它的属性要满足以下几个条件
- top大于等于0
- left大于等于0
- buttom小于等于视窗高度
- right小于等于视窗宽度
方法三 Intersetion Observer
创建观察者
const options = {
// 表示重叠面积占被观察者的比例,从 0 - 1 取值,
// 1 表示完全被包含
threshold: 1.0,
root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};
const callback = function(entries, observer) {
entries.forEach(entry => {
entry.time; // 触发的时间
entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
entry.boundingClientRect; // 被观察者的位置举行
entry.intersectionRect; // 重叠区域的位置矩形
entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.target; // 被观察者
});
};
const observer = new IntersectionObserver(callback, options);
传入的callback会在重叠比例超过threshold时执行
传入被观察者
const target = document.querySelector('.target');
observer.observe(target);
这个封装的虚拟列表是参考掘金上react进阶指南作者的写法 性能拉满 代码如下
function VirtualList(){
const [ dataList,setDataList ] = React.useState([]) /* 保存数据源 */
const [ position , setPosition ] = React.useState([0,0]) /* 截取缓冲区 + 视图区索引 */
const scroll = React.useRef(null) /* 获取scroll元素 */
const box = React.useRef(null) /* 获取元素用于容器高度 */
const context = React.useRef(null) /* 用于移动视图区域,形成滑动效果。 */
const scrollInfo = React.useRef({
height:500, /* 容器高度 */
bufferCount:8, /* 缓冲区个数 */
itemHeight:60, /* 每一个item高度 */
renderCount:0,
})
React.useEffect(()=>{
const height = box.current.offsetHeight /* 可视区高度*/
const { itemHeight , bufferCount } = scrollInfo.current /* 解构 */
/*重新计算渲染区个数 */
const renderCount = Math.ceil(height / itemHeight) + bufferCount
/* 对滚动区域属性重新赋值*/
scrollInfo.current = { renderCount,height,bufferCount,itemHeight }
/* 生成测试数据列表*/
const dataList = new Array(10000).fill(1).map((item,index)=> index + 1 )
setDataList(dataList)
/*position的值就是可视区+缓冲区的起始和结束索引*/
setPosition([0,renderCount])
},[])
const handleScroll = () => {
const { scrollTop } = scroll.current
const { itemHeight , renderCount } = scrollInfo.current
/*接下来的这两句代码就是这个虚拟列表点睛之处了 优化无敌*/
/*它的作用是如果当前列表子项没有完全离开可视区 那么你只能看到滚轮在滑动 实际上的内容区是没有滑动的 只有当你的整个列表子项完全离开可视区 内容区才下滑一个列表子项的高度*/
const currentOffset = scrollTop - (scrollTop % itemHeight)
/* 偏移,造成下滑效果 */
context.current.style.transform = `translate3d(0, ${currentOffset}px, 0)`
/* 计算可视区的起始索引*/
const start = Math.floor(scrollTop / itemHeight)
const end = Math.floor(scrollTop / itemHeight + renderCount + 1)
if(end !== position[1] || start !== position[0] ){ /* 如果render内容发生改变,那么重新截取可视区+缓冲区的起始和结束索引 */
setPosition([ start , end ])
}
}
const { itemHeight , height } = scrollInfo.current
const [ start ,end ] = position
const renderList = dataList.slice(start,end) /* 渲染区间 */
console.log('渲染区间',position)
return <div className="list_box" ref={box} >
<div className="scroll_box" style={{ height: height + 'px' }} onScroll={ handleScroll } ref={scroll} >
<div className="scroll_hold" style={{ height: `${dataList.length * itemHeight}px` }} />
<div className="context" ref={context}>
{
renderList.map((item,index)=> <div className="list" key={index} > {item + '' } Item </div>)
}
</div>
</div>
</div>
}
测试如下
双列表联动
代码写得有点乱,我就不全部帖上来了,说一下大致的思路吧,双列表联动最重要的就做好数据的来回传递。
为了方便两个列表交互,我设计的是把右侧字母列表当作城市列表的子组件,父组件给子组件传递了三个参数
1.第一个参数 activeStart是用来控制右侧字母高亮效果的,传过去的值是城市列表当前可视区出现的第一个字母列表项(可能不太合理,但是也挺好用的)
2.第二个参数是右侧列表要渲染的字母列表数据,因为有的数据可能有的字母下没有城市,就不用渲染这个字母了
3.第三个参数是一个函数,用来接收儿子组件即右侧列表组件点击对应字母索引带回来的值,拿到这个值之后,我只需把城市列表可视区的第一个列表项变为这个值就可以实现跳转效果了(本来我给它加了动画让城市列表平滑的滚动到那个字母位置,但是逻辑不太好写,效果不咋地,就删掉了,用者可自由发挥加上。)
传过来之后子组件接住
路由组件传参
这个知识点相信大家都很熟悉,之所以放在这里,是因为我想通过它与大家讨论一下,这个组件更佳的设计方案。
- 方案一 就把它当做路由组件,当你点击城市定位的时候,就让它跳转到这个路由,同时也把每一个列表项设置为一个Link,因为你用这个组件,总归是要实现城市切换功能的,所以就干脆给Link添加一个to属性,让它跳到相应的路由,同时也把点击的城市名字传递过去。这样点击城市列表项实现城市切换的功能代码就非常简单了,逻辑也比较简单。
- 方案二 把它设计成普通的组件,在你需要使用的页面应用它,设置一个状态来控制它的展示,再给它添加一个动画,当你需要选择城市的时候,修改这个状态的值,让它从底部平滑的向上呈现(类似于模态框),当点击了城市列表的某个城市子项之后,再让它隐藏,同时修改城市的名字为你选择的那个城市,整个过程没有路由跳转,都在你使用这个组件的路由页面下进行。
结束语
好了,本次分享到就结束了,希望能对React学习中的读者有一些帮助。同时,因为个人能力原因,所写的组件还有一些小Bug需要调整,也还有许多需要改进的地方,后面会持续优化更新代码,也希望大佬们能在评论区指正,也就设计方案这个问题谈谈大佬们的见解。