Taro实现VirtualList虚拟列表
在使用Taro开发微信小程序时,需要加载长列表数据,在官网找了相关的VirtualList虚拟列表的组件,要么版本过低(项目中使用3.0.1版本),要么使用不方便(可能是自己没看懂的问题),官方也说有虚拟列表就是长列表加载,使用后发现性能不能达到满足,于是就参考网上的虚拟列表的思路开始自己做。
简单说下思路,设计思路是通过虚拟列表,只展示屏幕可视区域范围的数据,但在taro中效果还是有点卡顿,经大佬指点,渲染可视区域时,
渲染组件不再进行删除和新建,而是创建指定数量的组件展示,向下滚动时,将最上面的组件通过css修改样式显示的位置,
形成一种链条滚动的形式。
简化的思路图:
下面贴出主要的代码:
1 /** 2 * author: wang.p 2021-09-18 3 * 4 * description: 自定义虚拟列表 5 * 6 * */ 7 8 import React, {Component} from "react" 9 import {ScrollView, View} from '@tarojs/components' 10 import PropsType from 'prop-types'; 11 import classnames from 'classnames'; 12 import './virtual-list.scss'; 13 14 class VirtualList extends Component { 15 16 static propTypes = { 17 className: PropsType.string, // 样式名 18 rowCount: PropsType.number, // 渲染的行数 19 source: PropsType.array, // 数据源数组 20 rowHeight: PropsType.number, // 行高 21 scrollToIndex: PropsType.number, // 跳转到指定的位置 22 getRowHeight: PropsType.func, // 动态行高 23 onScroll: PropsType.func, // 滚动处罚事件 24 onRowRender: PropsType.func, // 行渲染 25 onSrollTopRecommend: PropsType.func, // 触发顶部样式事件 这个是本人项目中使用的,可以不用 26 } 27 28 static defaultProps = { 29 rowCount: 20, 30 source: [], 31 rowHeight: 40 32 } 33 34 state = { 35 rowCount: 20, // 显示行数 36 scrollHeight: 0, // 所有内容渲染的高度 37 scrollData: [], // 渲染可视区域数据的数组 38 scrollStyles: [], // 样式数组 39 isCategoryToScroll: false, // 是否是分类切换定位滚动 40 scrollToIndex: 0, // 跳转到指定位置 41 compareHeight: 0 // 触发渲染的高度差 42 } 43 44 componentWillMount = () => { 45 const {rowCount, source, rowHeight, getRowHeight} = this.props; 46 let scrollStyles = []; 47 let scrollData = []; 48 let scrollHeight = 0; 49 let compareHeight = 0; 50 source.forEach((item, idx) => { 51 let styles = {position: 'absolute', left: 0, top: scrollHeight}; 52 scrollStyles.push(styles); 53 let tempHeight = typeof getRowHeight === 'function' ? getRowHeight(idx, item) : rowHeight; 54 scrollHeight += tempHeight; 55 }); 56 57 let showCount = source.length < rowCount ? source.length : rowCount; 58 for (let i = 0; i < showCount; i++) { 59 scrollData.push({sort: i, row: i}) 60 } 61 62 compareHeight = Math.floor(scrollStyles[showCount - 1].top / showCount) * 3; 63 64 this.setState({scrollHeight, scrollData, scrollStyles, rowCount, compareHeight}); 65 } 66 67 68 componentDidMount = () => { 69 70 } 71 72 componentWillReceiveProps(nextProps: Readonly<P>, nextContext: any) { 73 74 if (nextProps.scrollToIndex != this.state.scrollToIndex && this.state.scrollStyles.length > 0) { 75 let scrollToIndex = nextProps.scrollToIndex > this.state.scrollStyles.length ? this.state.scrollStyles.length - 1 : nextProps.scrollToIndex; 76 let scrollTop = this.state.scrollStyles[scrollToIndex]; 77 if (scrollTop) { 78 this.setState({scrollToIndex: nextProps.scrollToIndex, scrollTop: scrollTop.top , isCategoryToScroll: true}); 79 } 80 } 81 } 82 83 render() { 84 const {className, style } = this.props; 85 const {scrollHeight, scrollData, scrollStyles, scrollTop} = this.state; 86 87 return <ScrollView className={classnames('self-virtual-list', className)} 88 style={{...style}} 89 scrollTop={scrollTop} 90 scrollY={true} 91 scrollWithAnimation 92 onScroll={this.onScroll.bind(this)}> 93 <View className={'self-virtual-list-body'} style={{height: scrollHeight}}> 94 {scrollData.length > 0 && scrollData.map((item, idx) => { 95 return this.props.onRowRender(item, scrollStyles[item.row]); 96 })} 97 </View> 98 </ScrollView> 99 100 101 } 102 103 currentScrollTop = 0; 104 prevScrollTop = 0; // 记录上次滚动的Scrolltop 105 106 findMinOrMax = (data, isMax= false) => { 107 if (isMax) { 108 return data.reduce((prev, next) => { 109 if (prev.row < next.row) { 110 return next; 111 } else { 112 return prev; 113 } 114 }) 115 } else { 116 return data.reduce((prev, next) => { 117 if (prev.row < next.row) { 118 return prev; 119 } else { 120 return next; 121 } 122 }) 123 } 124 } 125 126 /** 127 * 滚动事件,计算渲染菜品的数据 128 * 滚动到顶部时,如果顶部有推荐菜品就展示出来,如果上拉滚动,就隐藏推荐菜品 129 * */ 130 onScroll = (event) => { 131 let scrollY = event.detail.deltaY; 132 const eventScrollTop = event ? event.detail.scrollTop : this.state.scrollTop; 133 const {scrollStyles, rowCount, compareHeight, scrollData, isCategoryToScroll} = this.state; 134 135 if (Math.abs(this.currentScrollTop - eventScrollTop) > compareHeight || eventScrollTop <= scrollStyles[3].top || eventScrollTop >= scrollStyles[scrollStyles.length - 5].top) { 136 // 查询出当前scrollTop在那个范围 137 this.currentScrollTop = eventScrollTop; 138 let scrollIndex = 0; 139 for(let i=1;i<scrollStyles.length;i++) { 140 if (scrollStyles[i - 1].top <= eventScrollTop && scrollStyles[i].top > eventScrollTop) { 141 scrollIndex = i; 142 break; 143 } 144 } 145 // 计算出渲染范围的最小下标和最大下标 146 let minIndex = scrollIndex - parseInt(Math.floor(rowCount / 2.0)); 147 if (minIndex < 0) { 148 minIndex = 0; 149 } 150 let maxIndex = minIndex + rowCount; 151 if (maxIndex > scrollStyles.length - 1) { 152 maxIndex = scrollStyles.length - 1; 153 } 154 // 找出当前显示的数据范围最小值和最大值 155 let minData = this.findMinOrMax(scrollData); 156 let maxData = this.findMinOrMax(scrollData, true); 157 let newScrollData = [...scrollData]; 158 if (minIndex > minData.row) { 159 // 向下滑动渲染, 找出最小值,替换成最大值,循环进行替换 160 let cycle = minIndex - minData.row; 161 for (let i = 0; i < cycle; i++) { 162 minData = this.findMinOrMax(scrollData); 163 maxData = this.findMinOrMax(scrollData, true); 164 165 scrollData[minData.sort]['row'] = maxData.row + 1; 166 } 167 168 this.setState({scrollData: newScrollData, isCategoryToScroll: false}); 169 } else { 170 // 向上滑动渲染 171 let cycle = minData.row - minIndex; 172 for (let i = 0; i < cycle; i++) { 173 minData = this.findMinOrMax(scrollData); 174 maxData = this.findMinOrMax(scrollData, true); 175 176 scrollData[maxData.sort]['row'] = minData.row - 1; 177 } 178 this.setState({scrollData: newScrollData, isCategoryToScroll: false}); 179 } 180 181 } 182 183 let scycelScroll = compareHeight / 3; 184 // 滚动一定距离,就触发外部事件 185 if (!isCategoryToScroll && Math.abs(this.prevScrollTop - eventScrollTop) > scycelScroll && this.props.onScroll) { 186 this.prevScrollTop = eventScrollTop; 187 let scrollIndex = 0; 188 for(let i=1;i<scrollStyles.length;i++) { 189 if (scrollStyles[i - 1].top <= eventScrollTop && scrollStyles[i].top > eventScrollTop) { 190 scrollIndex = i; 191 break; 192 } 193 } 194 this.props.onScroll(scrollIndex); 195 } 196 197 // 处理顶部隐藏的组件 198 if (this.props.onSrollTopRecommend) { 199 if (scrollY > 0) { 200 // 下拉 201 if (event.detail.scrollTop <= scycelScroll) { 202 // 展开推荐菜品 203 this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(true); 204 } 205 } else { 206 // 上拉 207 if (event.detail.scrollTop > scycelScroll) { 208 // 触发收起推荐菜品 209 this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(false); 210 } 211 } 212 } 213 214 } 215 216 217 } 218 219 220 export default VirtualList
在项目中的效果图:
使用方法说明:引入组件,添加对应的方法
<VirtualList className={'category-virtual'} width={width} height={height} source={source} rowCount={20} getRowHeight={this.getRowHeight} scrollToIndex={scrollIndex} onRowRender={this.onRowRender} onScroll={this.onScroll} />
/** * 获取行高 * @idx 数据源下标 * @value 数据 */ getRowHeight = (idx, value) => { // 这里可以通过数据源的下标idx,来返回高度 return 100; // 高度可以通过函数来计算 }
/** * @data 数据{sort: 渲染的行数的顺序, row: 渲染数据源的下标} * @style: 样式 * */ onRowRender = (data, style) => { const {sort, row} = data; const {source} = this.state; return <View key={sort} style={style}> ... </View> }
/** * 通过滚动 * @currentIndex 分类数组下标 * */ onScroll = (currentIndex) => { // 通过滚动的触发事件执行某些方法 }
下面提供代码下载路径:
链接:https://pan.baidu.com/s/1kW0w1D03N72uCE3S9Vy2zg
提取码:460i