我下列的所有代码都在该 Gitee 仓库中: https://gitee.com/ls1551724864/vue2-3-virtual-scroll-list
1、概述
-
一般长列表用在后端传递大量数据,要求前端进行展示的情况 -
先来看看原生的一个加载大量数据的一个情况: <div id="container"></div>
<script>
// 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
const container = document.getElementById("container");
for (let i = 0; i < 100000; i++) {
const div = document.createElement("div")
div.innerText = i
container.appendChild(div)
}
</script>
-
页面初次渲染完毕后,一旦后续发生DOM结构的变化,会出现重排和重绘、情况;绘制类的工作由浏览器的GUI渲染引擎执行,而JavaScript代码则是由JS引擎执行;由于渲染的机制,如果页面中存在大量的DOM渲染,可能导致网页出现“失去响应”的假象(白屏渲染)
2、计时
-
现在我使用console.time() 来进行一个代码执行时间的打印: <div id="container"></div>
<script>
// 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
const container = document.getElementById("container");
console.time('长列表')
for (let i = 0; i < 100000; i++) {
const div = document.createElement("div")
div.innerText = i
container.appendChild(div)
}
console.timeEnd('长列表')
</script>
补充:console.time() 与console.timeEnd() 是用来计算两个包裹的中间代码的执行时间,但是参数是对应起来的,只会计算这两个方法中参数一样的中间代码部分的执行时间。 -
来看一下页面打印结果: -
可以明显看出,打印出的结果是0.1s的时间,但是浏览器页面加载明显时间更长,所以注意,console.time() 打印的是JavaScript代码执行的时间,并不是页面渲染的时间
3、JavaScript线程
-
JavaScript是不是单线程的?
- 是单线程的,但是指的是JavaScript的主线程只有一个
-
常见的线程:
- JS引擎线程
- GUI渲染线程
- 事件触发器线程
- 时间触发器线程
- 网络请求线程
- Event Loop线程
-
所以想要查看页面渲染的时间,可以利用Event Loop的宏任务与微任务的原理来操作: <div id="container"></div>
<script>
// 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
const container = document.getElementById("container");
console.time('长列表')
for (let i = 0; i < 100000; i++) {
const div = document.createElement("div")
div.innerText = i
container.appendChild(div)
}
** setTimeout(() => {
console.timeEnd('长列表')
}, 0);**
</script>
-
上面我将console.timeEnd() 用定时器进行了包裹,所以让其代码执行进行了阻塞,所以打印出来的结果就是页面渲染的事件,可以看出时间明显比JavaScript代码执行长了很多
4、分片加载
-
分片加载的逻辑就是,设计一个计数器,每次做一个循环先渲染几百条几千条数据,然后让计数器进行累加,然后进行递归调用渲染数据的函数;当达到了目标数据条数的时候,就不再去执行该函数;其中对重要的一点:每次进行递归调用数据加载函数的时候,需要将其加入到宏任务队列当中,这样才不会造成渲染线程的阻塞,用户体验很好 -
看一下具体的代码: <div id="container"></div>
<script>
const container = document.getElementById("container");
// 1、记录加载到的位置
let index = 0
// 2、每次加载 500 条数据,一共加载 50w 条数据,一共加载 1000 次
// 封装一个加载数据的函数
function loadData() {
// 当 index 计数器大于了 50w,那么说明数据加载完毕,就不在进行页面的渲染
if (index >= 500000) return
// 使用 for 循环每次加载 500 条数据
for (let i = 0; i < 500; i++) {
const div = document.createElement('div')
div.innerHTML = i + index
container.appendChild(div)
}
// 让计数器 index + 500
index += 500
// 讲下一次递归调用,放在下一个宏任务中去执行
setTimeout(loadData, 0)
}
// 调用加载数据的函数
console.time('分片加载')
loadData()
setTimeout(() => {
console.timeEnd('分片加载')
}, 0);
</script>
-
看一下页面效果: -
从上图就可以看出,现在渲染50w条数据的时间明显少了很多,但是还存在一个缺陷:可以发现我每次拖动滚动条的时候,滚动条还在进行上移,说明后面的数据还在进行加载,这个当然对用户体验是很好的,但是对于浏览器来说,要创建50w个相同结构的DOM,这对于浏览器的性能来说还是影响比较大的,其实每次就让他加载固定个数的DOM节点即可,不要把50w条全部加载出来。
5、vue-virtual-scroll-list
-
这个插件就是vue中的一个长列表的插件,官网地址:https://tangbc.github.io/vue-virtual-scroll-list/#/ -
来看一下该组件的渲染情况: -
可以明显看出,其渲染的时候,DOM节点数量都是固定的,并不会将所有的内容全部加载出来
6、自己实现vue虚拟列表
① vue2
Ⅰ. 项目搭建
-
建一个新的文件夹,在这个文件夹中创建一个vue2的项目:vue create vue2-virtual-scroll ,模板选择默认的vue2模板即可; -
在components目录下创建一个List.vue 组件,用来进行虚拟列表的展示; -
在App.vue主入口页面中去引入该组件: <template>
<div id="app">
<List
:items="items"
:size="60"
:shownumber="10"
/>
</div>
</template>
<script>
import List from './components/List.vue'
export default {
name: 'App',
components: {
List
},
computed: {
// 要进行渲染的数据列表
items () {
// 自己模拟一万条数据,将其内容进行填充
return Array(10000).fill('').map((item, index) => ({
id: index,
content: '列表项内容' + index
}))
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
注意:
- 可以发现我的List组件上面有几个参数,分别介绍一下这几个参数的意义:
items :要进行渲染的列表数据;size :每一条数据的高度;showNumber :每次渲染的数据条数(DOM个数);
- 后续还可以继续给这个组件添加属性,用来决定一些数据的性质等
- 因为没有真实的数据,我在computed计算属性中,通过数组遍历的方式创建了一万条假数据,并且都填充上了值,让这数组中的值充当数据;
-
先把List.vue 虚拟列表页面组件搭建起来: <template>
<div
class="container"
:style="{ height: containerHeight }"
>
<!-- 数据列表 -->
<div class="list">
<!-- 列表项 -->
<div
v-for="item in showData"
:key="item.id"
:style="{ height: size + 'px' }"
>
{{ item.content }}
</div>
<!-- 用于撑开高度的元素 -->
<div
class="bar"
:style="{ height: barHeight }"
/>
</div>
</div>
</template>
<script>
export default {
name: 'VircualList',
props: {
// 要渲染的数据
items: {
type: Array,
required: true
},
// 每条数据渲染的节点的高度
size: {
type: Number,
required: true
},
// 每次渲染的 DOM 节点个数
shownumber: {
type: Number,
required: true
}
},
data () {
return {
start: 0, // 要展示的数据的起始下标
end: this.shownumber // 要展示的数据的结束下标
}
},
computed: {
// 最终筛选出的要展示的数据
showData () {
return this.items.slice(this.start, this.end)
},
// 容器的高度
containerHeight () {
return this.size * this.shownumber + 'px'
},
// 撑开容器内容高度的元素的高度
barHeight () {
return this.size * this.items.length + 'px'
}
}
}
</script>
<style scoped>
.container {
overflow-y: scroll;
background-color: rgb(150, 195, 238);
font-size: 20px;
font-weight: bold;
line-height: 60px;
}
</style>
注意几点:
- 接收父组件传递过来的数据,然后我声明了两个变量,
start 、end ,这两个就是为了每次进行渲染要显示的数据,在items数组中的起始结束下标位置;这两的长度固定在shownumber 个单位以内; - 可以发现我对
container 容器设置了一个高度,因为在真实的开发中,一般就是在一块区域中进行展示列表数据,所以把这个模拟成一个页面的小框框区域,我设置的其高度就是shownumber 个列表项的高度,刚好让shownumber 个数据完全展示出来; - 我在页面中还创建了一个类名为
bar 的div节点,这个是为了撑开整个容器的高度,让其有一个滚动的区域,高度就是整个items数据的长度×每个列表项的高度size -
先来看页面的效果:
Ⅱ. 虚拟列表制作
-
给容器绑定一个滚动事件,当容器发生滚动的时候,就让其动态的去渲染后续的数据 <template>
<div
class="container"
:style="{ height: containerHeight }"
@scroll="handleScroll"
ref="container"
>
<!-- 数据列表 -->
<div class="list">
<!-- 列表项 -->
<div
v-for="item in showData"
:key="item.id"
:style="{ height: size + 'px' }"
>
{{ item.content }}
</div>
<!-- 用于撑开高度的元素 -->
<div
class="bar"
:style="{ height: barHeight }"
/>
</div>
</div>
</template>
<script>
export default {
name: 'VircualList',
props: {
// 要渲染的数据
items: {
type: Array,
required: true
},
// 每条数据渲染的节点的高度
size: {
type: Number,
required: true
},
// 每次渲染的 DOM 节点个数
shownumber: {
type: Number,
required: true
}
},
data () {
return {
start: 0, // 要展示的数据的起始下标
end: this.shownumber // 要展示的数据的结束下标
}
},
computed: {
// 最终筛选出的要展示的数据
showData () {
return this.items.slice(this.start, this.end)
},
// 容器的高度
containerHeight () {
return this.size * this.shownumber + 'px'
},
// 撑开容器内容高度的元素的高度
barHeight () {
return this.size * this.items.length + 'px'
}
},
methods: {
// 容器的滚动事件
handleScroll () {
// 获取容器顶部滚动的尺寸
const scrollTop = this.$refs.container.scrollTop
// 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
// 起始的下标就是卷去的数据条数,向下取整
this.start = Math.floor(scrollTop / this.size)
// 结束的下标就是起始的下标加上要展示的数据条数
this.end = this.start + this.shownumber
}
}
}
</script>
<style scoped>
.container {
overflow-y: scroll;
background-color: rgb(150, 195, 238);
font-size: 20px;
font-weight: bold;
line-height: 60px;
}
</style>
注意:
- 在每次滚动的时候,就需要去修改要重新渲染的数据的起始和结束下标:
- 起始下标的计算 = 区域向上卷去的高度
scrollTop ÷每个数据的高度 size ,然后向下取整 - 结束下标的计算 = 起始的下标 + 页面展示的数据的条数
shownumber -
可以发现上图中,数据发生了变化,但是列表还是依旧向上滚动,接下来需要给列表做定位的处理,只需要每次滚动的时候,让列表跟着向下滚动即可 <template>
<div
class="container"
:style="{ height: containerHeight }"
@scroll="handleScroll"
ref="container"
>
<!-- 数据列表 -->
<div
class="list"
:style="{ top: listTop }"
>
<!-- 列表项 -->
<div
v-for="item in showData"
:key="item.id"
:style="{ height: size + 'px' }"
>
{{ item.content }}
</div>
<!-- 用于撑开高度的元素 -->
<div
class="bar"
:style="{ height: barHeight }"
/>
</div>
</div>
</template>
<script>
export default {
name: 'VircualList',
props: {
// 要渲染的数据
items: {
type: Array,
required: true
},
// 每条数据渲染的节点的高度
size: {
type: Number,
required: true
},
// 每次渲染的 DOM 节点个数
shownumber: {
type: Number,
required: true
}
},
data () {
return {
start: 0, // 要展示的数据的起始下标
end: this.shownumber // 要展示的数据的结束下标
}
},
computed: {
// 最终筛选出的要展示的数据
showData () {
return this.items.slice(this.start, this.end)
},
// 容器的高度
containerHeight () {
return this.size * this.shownumber + 'px'
},
// 撑开容器内容高度的元素的高度
barHeight () {
return this.size * this.items.length + 'px'
},
// 列表向上滚动时要动态改变 top 值
listTop () {
return this.start * this.size + 'px'
}
},
methods: {
// 容器的滚动事件
handleScroll () {
// 获取容器顶部滚动的尺寸
const scrollTop = this.$refs.container.scrollTop
// 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
// 起始的下标就是卷去的数据条数,向下取整
this.start = Math.floor(scrollTop / this.size)
// 结束的下标就是起始的下标加上要展示的数据条数
this.end = this.start + this.shownumber
}
}
}
</script>
<style scoped>
.container {
position: relative;
overflow-y: scroll;
background-color: rgb(150, 195, 238);
font-size: 20px;
font-weight: bold;
line-height: 60px;
text-align: center;
}
.list {
position: absolute;
top: 0;
width: 100%;
}
</style>
注意:列表动态的高度top是当前页面渲染的数据的起始下标 × 每个数据的高度, 即卷上去的列表高度 -
来看现在的页面效果: -
上面就可以很清楚的看出列表项似乎是一直在向下滚动的,但是页面的DOM节点数一直没有改变。
② vue3
Ⅰ. 项目搭建
- 使用vite搭建项目:
npm init vite@latest - 项目命名:
vue3-virtual-scroll - 然后进入到该项目中,需要安装依赖:
npm install - 运行项目:
npm run dev - 后面的配置选择vue相关的基础配置即可
Ⅱ. 虚拟列表制作
-
这里不再很详细的说明了,其逻辑与上述vue2的制作过程一样,知识语法不一样而已,我只将最重要的两个页面的代码贴出来:App.vue和List.vue -
App.vue 页面中的代码: <template>
<div id="app">
<List
:items="items"
:size="60"
:shownumber="10"
/>
</div>
</template>
<script setup>
// ------------------------------------- 导入模块 ----------------------------------
// 导入 vue3 的 API
import { computed } from 'vue'
// 导入列表组件
import List from './components/List.vue'
// ------------------------------------- 声明数据 ----------------------------------
// 模拟要进行渲染的数据列表
const items = computed(() => {
// 自己模拟一万条数据,将其内容进行填充
return Array(10000).fill('').map((item, index) => ({
id: index,
content: '列表项内容' + index
}))
})
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
-
List.vue 页面中的代码: <template>
<div
class="container"
:style="{ height: containerHeight }"
@scroll="handleScroll"
ref="container"
>
<!-- 数据列表 -->
<div
class="list"
:style="{ top: listTop }"
>
<!-- 列表项 -->
<div
v-for="item in showData"
:key="item.id"
:style="{ height: size + 'px' }"
>
{{ item.content }}
</div>
<!-- 用于撑开高度的元素 -->
<div
class="bar"
:style="{ height: barHeight }"
/>
</div>
</div>
</template>
<script setup>
// ------------------------------------- 导入模块 ----------------------------------
// 导入 vue3 的 API
import { ref, toRefs, computed } from 'vue'
// ------------------------------------- 组件传值 ----------------------------------
// 接收父组件传递的数据
const props = defineProps({
// 要渲染的数据
items: {
type: Array,
required: true
},
// 每条数据渲染的节点的高度
size: {
type: Number,
required: true
},
// 每次渲染的 DOM 节点个数
shownumber: {
type: Number,
required: true
}
})
// 使用 toRefs 包裹 props,让解构获得的父组件传递的参数变为响应式的
const { items, size, shownumber } = toRefs(props)
// ------------------------------------- 声明变量 ----------------------------------
const container = ref(null) // 页面 container 节点
let start = ref(0) // 要展示的数据的起始下标
let end = ref(shownumber.value) // 要展示的数据的结束下标
// ------------------------------------- 计算属性 ----------------------------------
const showData = computed(() => items.value.slice(start.value, end.value)) // 最终筛选出的要展示的数据
const containerHeight = computed(() => size.value * shownumber.value + 'px') // 容器的高度
const barHeight = computed(() => size.value * items.value.length + 'px') // 撑开容器内容高度的元素的高度
const listTop = computed(() => start.value * size.value + 'px') // 列表向上滚动时要动态改变 top 值
// ------------------------------------- 声明函数 ----------------------------------
// 容器的滚动事件
const handleScroll = () => {
// 获取容器顶部滚动的尺寸
const scrollTop = container.value.scrollTop
// 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
// 起始的下标就是卷去的数据条数,向下取整
start.value = Math.floor(scrollTop / size.value)
// 结束的下标就是起始的下标加上要展示的数据条数
end.value = start.value + shownumber.value
}
</script>
<style scoped>
.container {
position: relative;
overflow-y: scroll;
background-color: rgb(150, 195, 238);
font-size: 20px;
font-weight: bold;
line-height: 60px;
text-align: center;
}
.list {
position: absolute;
top: 0;
width: 100%;
}
</style>
|