задний план
То есть только виртуальный список рендеринга данных видимой области, так что в случае большого списка данных, только область визуализации данных, верхняя и нижняя невидимая область домота вместо пустой (пустой), чтобы мы могли значительное снижение В количестве рендеринга DOM, поэтому список может быть гладко бесконечным прокруткой, который очень важен в мобильном терминале.
План реализации
Решение реализации виртуального списка состоит в том, чтобы вычислить, какие данные отображать в видимой области, а затем взять эту часть данных из полного списка, а для того, чтобы список выглядел не усеченным, необходимо правильно рассчитать высоту верхнего и нижнего пробелов, чтобы виртуальные списки можно было прокручивать так же, как и настоящие списки.
Исходя из этого, нам нужно сделать три вещи:
1. Необходимо рассчитать высоту невидимой области вверху и внизу, чтобы поддерживалась вся область списка, чтобы ее высота была такой же, как и при неусечении данных, мы называем эти две высоты.topHeight
,bottomHeight
.
2. Как рассчитать начальную позицию усеченных данныхstart
и конечное положениеend
, данные, отображаемые в видимой области,list.slice(start, end)
.
3. Постоянно обновлять во время прокрутки доtopHeight
,bottomHeight
,start
,end
, который обновляет элементы списка, отображающие видимую область. Конечно, нам нужно сравнить старое начало и конец, если начало и конец не изменились, то обновлять не нужно, иначе вид будет постоянно обновляться в реакции, чтобы страница постоянно тряслась (на самом деле только начало сравнивается, но при инициализации все наши значения равны 0, и первый расчет будет начинаться как 0, но конец не равен 0, на этот раз его необходимо обновить, поэтому сравните два). Конечно, вы можете избежать ненужного рендеринга, оценивая componentShouldMount.
Вычисление topHeight относительно простое, то есть сколько прокручивается, т.е.topHeight = scrollTop
.
Вычисление start зависит от topHeight и высоты каждого элемента списка.itemHeight
, Предположим, мы прокручиваем элемент списка вверх, тогда наше начало равно 1, прокручиваем вверх два, начало равно 2, поэтому мы знаемstart = Math.floor(topHeight / itemHeight)
, округление в меньшую сторону необходимо для того, чтобы начальное значение не было слишком большим и не приводило к уменьшению отображения, чтобы вверху оставалось пустое пространство.
Конечные вычисления зависит от высоты экрана можно отобразить список номеров элементов, которые мы называемvisibleCount
, то естьvisibleCount = Math.ceil(clientHeight / itemHeight)
, округление делается для того, чтобы вычисление не было слишком маленьким и на экране не отображалось достаточное количество контента, тогдаend = start + visibleCount
.
bottomHeight требует, чтобы мы знали высоту всего списка, прежде чем он будет усечен, вычесть высоту его вершины и вычислить высоту вершины. Очень просто получить конец. Предполагая, что количество элементов всего нашего списка равно TotalItem, затемbottomHeight = (totalItem - end - 1) * itemHeight
.
На данный момент мы знаем, как вычислять различные данные для реализации нашего виртуального списка, что является самой простой реализацией.
проблемы
Но когда вы реализуете это так, вы обнаружите две проблемы:
1. При прокрутке вверху или внизу видимой области будет пустое пространство.
2. Каждый раз, когда вы прокручиваете до точки, где вам нужно заменить пустое место фактическим элементом списка, страница будет трястись. Причина в том, что высота каждого элемента списка непоследовательна. Когда вы хотите заменить замененный список item больше или меньше itemHeight, и он заменяется в видимой области, браузер будет трястись, это решение можно заменить опережением времени замены, то есть заменой вверху нашего невидимого.
Давайте проанализируем, для первого вопроса будет пустое место, тогда мы можем зарезервировать определенную позицию вверху или внизу, а для второго вопроса мы также можем зарезервировать определенное место вверху и внизу, Итак, чтобы решить эту проблема может быть решена только одним решением,То есть верх и низ отведены под определенную позицию.
ПредположениеreserveTop
количество позиций, зарезервированных для топа,reserveBottom
Количество позиций, отведенных под низ, тогда расчет приведенных выше данных будет переопределен, подробнее см. рисунок ниже.
ReserveTop и ReserveBottom должны быть как можно больше (конечно, не слишком большими), или, если вы знаете максимальную высоту элемента списка, просто используйте эту максимальную высоту. Когда вы обнаружите, что при прокрутке вверху есть пробел, увеличьте значение ReserveTop.Когда вы обнаружите, что внизу при прокрутке есть пробел, увеличьте значение ReserveBottom.
код
Так как я понял эту бесконечную прокрутку, когда я был в контакте с проектом реакции, я взял реакцию в качестве примера. Абстрагированный в компонент Scroll, этот компонент отвечает за вычисления.topHeight
,bottomHeight
,start
,end
, а затем обновите его в родительский компонент. Родительский компонент получает данные для усечения данных и установить верхние и нижние пустые высоты для реализации виртуального списка.
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {throttle, touchBottom,
getClientWidth, getClientHeight} from '@/js/util'
// 顶部底部预留位置数,解决滚动出现留白的问题和抖动问题
const reserveBottom = 5 // 底部预留个数
const reserveTop = 3 // 顶部预留个数
export default class Scroll extends Component {
constructor(props) {
super(props)
this.state = {
container: props.container,
start: 0, // 列表中滚动到可视区域的开始索引
end: 0, // 结束索引
topHeight: 0, // 上面被隐藏部分的高度
bottomHeight: 0 // 下面被隐藏部分的高度
}
}
static propTypes = {
container: PropTypes.string, // 滚动容齐的selector,不传的时候为window,即以整个窗口为滚动区域
itemHeight: PropTypes.number.isRequired, // 每个列表项的高度,实现虚拟列表需要计算列表项高度
update: PropTypes.func.isRequired, // 滚动更新函数
getList: PropTypes.func.isRequired, // 触底拉数据的函数
totalItem: PropTypes.number.isRequired // 列表总共多少个项目
}
// 更新可见区域的数据,实现虚拟列表
updateVisibleList () {
let { itemHeight, totalItem } = this.props
// 顶部空白区高度,减法是为了预留顶部位置,一个是防止用户向上拉的时候出现空白,一个是为了防止滚动停止时页面抖动
let topHeight = this.getScrollTop() - itemHeight * reserveTop
let start = Math.floor(topHeight / itemHeight)
start = start < 0 ? 0 : start
// 一屏显示多少个
const visibleCount = Math.ceil(getClientHeight() / itemHeight)
let end = start + visibleCount + reserveBottom
let {start: oldStart, end: oldEnd} = this.state
// 不用更新,除了可以避免无谓的dom更新外,还可以防止抖动
if (start === oldStart && end === oldEnd) {
return
}
// 底部空白区高度
const bottomHeight = (totalItem - 1 - end) * itemHeight
this.setState({
start,
end
})
this.props.update({
start,
end,
topHeight,
bottomHeight
})
}
componentDidMount () {
// 虚拟列表 and 触底拉数据
this.onScroll = throttle(() => {
this.updateVisibleList()
if (touchBottom(this.$el)) {
this.props.getList()
}
}, 30)
this.$el = document.querySelector(this.state.container) || window
this.$el.removeEventListener('scroll', this.onScroll)
this.$el.addEventListener('scroll', this.onScroll)
this.getScrollTop = (() => {
let $el = this.$el.self === this.$el ? document.documentElement : this.$el
return function() {
return $el.scrollTop
}
})()
this.updateVisibleList()
}
componentWillUnmount () {
// 防止内存泄漏
this.$el.removeEventListener('scroll', this.onScroll)
}
render () {
return (
<div className="scrollList">
{this.props.children}
</div>
)
}
}
// 使用例子
import Scroll from './Scroll'
import React, {Component} from 'react'
import {getEvents} from 'api/events'
export default class List extends Component {
constructor(props) {
super(props)
this.state = {
list: [],
hasMore: true,
pageNo: 0,
limit: 25,
isLoading: false,
// 实现虚拟列表
start: 0,
end: 0,
maxItemHeight: 232,
topHeight: 0,
bottomHeight: 0
}
this.getList()
}
// 获取列表,初始化 or 下拉加载
getList () {
if (this.state.isLoading || !this.state.hasMore) return
let {pageNo, limit, list} = this.state
this.setState({
isLoading: true
})
getEvents({
offset: pageNo * limit,
limit
}).then(res => {
this.setState({
isLoading: false
})
if (res.error) {
Message.show({
message: res.msg
})
return
}
let pageNo = pageNo + 1
let {events, hasMore} = res
this.setState({
list: list.concat(events),
hasMore: events.length === 0 ? false : hasMore,
pageNo: pageNo,
end: this.state.end + 1
})
})
}
// scroll组件更新可视区域的列表项
update (data) {
this.setState(data)
}
render () {
let {start, end, topHeight, bottomHeight, maxItemHeight as itemHeight} = this.state
const list = this.state.list.slice(start, end)
let listHtml = []
// 显示列表
if (list.length === 0) {
listHtml = (<li className={styles.empty}>
<i></i>
<p>No data</p>
</li>)
} else {
listHtml.push(<li key="first" style={{height: start === 0 ? 0 : topHeight}}></li>)
listHtml = listHtml.concat(list.map((item) => {
return (<li key={item.id} className="clearfix">
{item.title}
</li>)
}))
listHtml.push((<li key="last" style={{height: bottomHeight}}></li>))
}
return (
<div className="content">
<Scroll itemHeight={itemHeight} totalItem={this.state.list.length}
update={this.update.bind(this)} getList={this.getList.bind(this)}>
<div className={styles.container}>
{listHtml}
</div>
</Scroll>
</div>
)
}
}
Суммировать
Для реализации виртуального списка необходимо правильно рассчитывать элементы списка в видимой области, а также высоту верхних и нижних пробелов, при этом необходимо избегать появления пустых частей в видимой области. и дрожание страницы при прокрутке, и решается резервированием верхней и нижней позиции.эти два вопроса.
В этой статье представлено решение для реализации виртуальных списков. Если у вас есть лучшие решения, не стесняйтесь оставлять сообщения для предложений.