我下列的所有代码都在该 Gitee 仓库中:
https://gitee/ls1551724864/vue2-3-virtual-scroll-list
文章目录
- 1、概述
- 2、计时
- 3、JavaScript线程
- 4、分片加载
- 5、vue-virtual-scroll-list
- 6、自己实现vue虚拟列表
- ① vue2
- Ⅰ. 项目搭建
- Ⅱ. 虚拟列表制作
- ② vue3
- Ⅰ. 项目搭建
- Ⅱ. 虚拟列表制作
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组件上面有几个参数,分别介绍一下这几个参数的意义:
-
先把
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>
更多推荐
vue2 与 vue3 虚拟列表实现
发布评论