最近在做移动端瀑布流效果,使用v-infinite-scroll的时候遇到了渲染乱序的问题
在移动端的产品列表页中先请求第一页的数据进行渲染 后续再随着用户滑动请求接下来页码的数据,首先在相应div上使用了v-infinite-sroll 定义load函数
<div
v-infinite-scroll="load"
infinite-scroll-distance="100"
>
<div
v-for="data in dataList"
:key="data.id"
>
……
</div>
</div>
<script>
export default {
data {
return {
dataList: [] // 用于渲染的数据
……
}
},
async mounted() {
this.reqMsg.current = 1 // 先请求第一页数据
let res = await getDataList(this.$axios, this.reqMsg)
if(res.status == 200) { // 请求成功
this.handleData(res.dataList) // 进行数据处理
}
},
methods: {
handleData(newDataList) {
// 渲染前进行数据处理
let dataList = this.dataList // 接收旧数据
newDataList.forEach(item => {
dataList .push(item) // 追加新数据
})
……
this.dataList = dataList // 赋值
},
// 每次距离底部100px时触发加载函数
load() {
this.reqMsg.current = this.reqMsg.current + 1 // 请求页码加1
let res = await getDataList(this.$axios, this.reqMsg)
if(res.status == 200) { // 请求成功
this.handleData(res.dataList) // 进行数据处理
}
}
}
}
</script>
通过以上逻辑就可以简单实现移动端瀑布流效果,效果就是进入页面先加载第一页数据,随用户滚动行为再陆续请求后面页码的数据一直到请求完,新请求的数据会在原来渲染的基础上继续渲染新页码的数据。
但是在应用到大数据量的瀑布流时发现了一个问题,就是渲染不是按照页码进行的,有时候最后一页的数据会渲染在前面的位置而不是页面底部,记录到的有一次渲染是按照1、2、3、4、5、6、7、10、8、9进行的。于是开始重新检查代码逻辑,只找到了一种解释,由于滑动屏幕速度很快,load函数在短时间内被连续触发,但是响应的速度是没办法控制的,这跟很多因素都关联,就会造成有时候后面页码的数据先响应回来了,就比如例子中的第10页数据,这时关于第10页的load函数执行就会接着往下走,进入数据处理函数,由于第10页的数据先被处理好赋值到了this.dataList,数据改变驱动视图更新,此时页面上第10页的数据就会被追加呈现在第7页数据的后面,导致了渲染没有按照我们想要的效果呈现。
那么如何解决这一问题呢,首先清楚我们是没办法控制响应顺序的,就算请求是按次序发送的,响应也不一定,那么只能在数据处理的环节进行控制,赋值的时候保证数据的排序,那么渲染也能保证顺序了。代码如下:
<div
v-infinite-scroll="load"
infinite-scroll-distance="100"
>
<div
v-for="data in dataList"
:key="data.id"
>
……
</div>
</div>
<script>
export default {
data {
return {
dataList: [] // 用于渲染的数据,
currentRenderPage: 2; // 表明当前等待渲染的页码
responseDataList: []; // 用于保存提前响应回来的数据
……
}
},
async mounted() {
this.reqMsg.current = 1 // 先请求第一页数据
let res = await getDataList(this.$axios, this.reqMsg)
if(res.status == 200) { // 请求成功
this.handleData(res.dataList) // 进行数据处理
}
},
methods: {
// 每次距离底部100px时触发加载函数
load() {
this.reqMsg.current = this.reqMsg.current + 1 // 请求页码加1
let res = await getDataList(this.$axios, this.reqMsg)
if(res.status == 200) { // 请求成功
// 假设后端返回数据包含当前响应的页码 如res.current,则直接传参
// 如果没有用请求参数的current即可 只要能代表当前响应的页码就可以
// 假设后端返回数据包含总页数,如res.pages,则直接传参
this.handleData(res.dataList, res.current, res.pages) // 进行数据处理
}
},
// 传入当前响应的页码 表明正在处理第几页数据
handleData(newDataList, responsePage, totalPages) {
if(responsePage != undefined) {
// 表明从laod函数而来
let dataList = this.dataList // 接收旧数据
if(this.currentRenderPage == responsePage) {
// 表明当前等待渲染的页码和响应过来的一致 直接追加渲染
newDataList.forEach(item => {
dataList .push(item) // 追加新数据
})
} else {
// 表明当前响应页码大于当前等待渲染的页码
// 当前等待渲染页码小于等于总页数 大于总页数的页码就不需要等待渲染了
if(this.currentRenderPage <= totalPages) {
let responseDataList = this.responseDataList // 接收之前存的提前响应回来的数据
if(newDataList.length > 0) { // 响应的数据非空
// 将提前响应回来的数据存起来 记录响应页码
responseDataList.push({
index: responsePage,
list: newDataList
})
}
if(responseDataList.length > 0) {
let currentDataList = [] // 保存从responseDataList中取出来的数据
let deleteIndex = -1 // 保存要取出来的数据对应的索引 取出来以后进行删除
responseDataList.forEach((item,i) => {
// 如果responseDataList有等待渲染页面的数据
if(item.index == this.currentRenderPage) {
currentDataList = item.list
deleteIndex = i
}
})
currentDataList.forEach((item,index) => {
dataList.push(item) // 追加新数据
})
}
if(deleteIndex != -1) { // 表明已经取到了要渲染的数据
this.currentRenderPage = this.currentRenderPage + 1 // 继续等待下一页的数据
responseDataList.splice(deleteIndex, 1) // 取到了以后删掉
}
this.responseDataList = responseDataList // 更新responseDataList数组
}
}
} else {
// 首页处理数据时未传参按照原处理逻辑即可
let dataList = this.dataList // 接收旧数据
newDataList.forEach(item => {
dataList .push(item) // 追加新数据
})
……
this.dataList = dataList // 赋值
}
}
}
}
</script>
总体思想就是等待正确页码的数据,如果响应本来就是正确的直接赋值触发渲染,如果不是则先暂存,同时从以往暂存过的数据里查找是否有正确页码的数据,有的话赋值触发渲染,没有则结束。在一遍遍中触发执行中就能保证渲染是按照页码顺序的。
实在也没想到更好的方法解决,各位大佬们如果有更好的方法欢迎指导!