在使用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