vue實(shí)現(xiàn)虛擬列表組件解決長列表性能問題
最近項(xiàng)目中需要用到列表的展示,且不分頁。當(dāng)數(shù)據(jù)加載太多時(shí)會(huì)造成性能問題。因此采用虛擬列表來優(yōu)化
一、虛擬列表
真實(shí)列表:每條數(shù)據(jù)都展示到html上,數(shù)據(jù)越多,DOM
元素也就越多,性能也就越差。
虛擬列表:只展示部分?jǐn)?shù)據(jù)(可見區(qū)域展示數(shù)據(jù)),當(dāng)屏幕滾動(dòng)時(shí)替換展示的數(shù)據(jù),DOM
元素的數(shù)量是固定的,相比較真實(shí)列表更高效。
二、實(shí)現(xiàn)思路
難點(diǎn)與思考:
1. 如何計(jì)算需要渲染的數(shù)據(jù)
- 數(shù)據(jù)可分為總數(shù)據(jù),與需要
渲染的數(shù)據(jù)
,需要渲染的數(shù)據(jù)包括了可見區(qū)域與緩沖區(qū)域的數(shù)據(jù) - 通過單條數(shù)據(jù)占位的高度與可見區(qū)域的高度,算出可見區(qū)域的列表?xiàng)l數(shù),再往上和往下擴(kuò)展幾條緩沖區(qū)域的數(shù)據(jù)(本次代碼是以3倍可見區(qū)域的條數(shù)作為需要渲染的數(shù)據(jù)條數(shù))
2. 何時(shí)替換數(shù)據(jù)
- 監(jiān)聽滾動(dòng)事件,渲染元素的第一條數(shù)據(jù)滾動(dòng)出緩沖區(qū)域后(也就是可見區(qū)域第一個(gè)元素的
index
大于緩沖區(qū)域的條數(shù)時(shí)),就開始替換數(shù)據(jù)了,每次往上滑動(dòng)一個(gè)元素,就替換一次數(shù)據(jù)。
3. 為何需要空白占位,如何計(jì)算空白占位的高度
- 由于列表在滾動(dòng)過程中會(huì)替換數(shù)據(jù),如果沒有空白占位的話,會(huì)導(dǎo)致第一個(gè)元素消失后,第二個(gè)元素立馬替換了第一個(gè)元素的位置,會(huì)導(dǎo)致錯(cuò)位。如下圖所示:
- 因此滾動(dòng)時(shí),需要在元素消失后,補(bǔ)一個(gè)相同高度的空白占位
- 上方的空白占位 = 消失的元素個(gè)數(shù)(也就是第一個(gè)渲染元素的
index
) * 單個(gè)元素的高度 - 下方的空白占位 = 剩下需要渲染的元素個(gè)數(shù)(也就是最后一個(gè)元素的
index
與總數(shù)據(jù)條數(shù)的差值)* 單個(gè)元素的高度
其他注意事項(xiàng):
- 在使用
v-for
遍歷渲染數(shù)據(jù)時(shí),key
的值使用index
,不用item
的id
,可以避免該dom元素被重新渲染,只替換數(shù)據(jù)。 - 下拉加載更多時(shí),不要將整個(gè)數(shù)據(jù)替換了,而是追加到數(shù)據(jù)的后面,避免之前展示的數(shù)據(jù)被替換了。
- 空白占位可以使用
padding
來占位,也可以使用DOM元素占位,使用DOM元素占位監(jiān)聽滾動(dòng)事件時(shí),應(yīng)使用touchmove
或mousemove
監(jiān)聽,避免dom元素高度變化后,又觸發(fā)了scroll
滾動(dòng)事件。 - 監(jiān)聽滾動(dòng)事件應(yīng)該采用節(jié)流的方式,避免程序頻繁執(zhí)行。
- 監(jiān)聽滾動(dòng)時(shí)加上
passive
修飾符,可以提前告知瀏覽器需要執(zhí)行preventDefault
,使?jié)L動(dòng)更流暢,具體功能可以參考vue官網(wǎng)。 - 外層包裹的元素需要有固定高度,并且
overflow
為auto
,才能監(jiān)聽scroll
滾動(dòng)事件。
三、實(shí)現(xiàn)
最終實(shí)現(xiàn)效果
實(shí)現(xiàn)代碼
<template> <div id="app"> <!-- 監(jiān)聽滾動(dòng)事件使用passive修飾符 --> <div class="container" ref="container" @scroll.passive="handleScroll"> <div :style="paddingStyle"> <!-- key使用index,可避免多次渲染該dom --> <div class="box" v-for="(item, index) in showList" :key="index"> <h2>{{ item.title }} - {{ item.id }}</h2> <h3>{{ item.from }}</h3> </div> <div>到低了~~~</div> </div> </div> </div> </template> <script> import axios from "axios"; export default { name: "App", data() { return { allList: [], // 所有數(shù)據(jù) isRequest: false,// 是否正在請(qǐng)求數(shù)據(jù) oneHeight: 150, // 單條數(shù)據(jù)的高度 showNum: 0, // 可見區(qū)域最多能展示多少條數(shù)據(jù) startIndex: 0, // 渲染元素的第一個(gè)索引 canScroll: true, // 可以監(jiān)聽滾動(dòng),用于節(jié)流 scrollTop: 0,// 當(dāng)前滾動(dòng)高度,再次返回頁面時(shí)能定位到之前的滾動(dòng)高度 lower: 150,// 距離底部多遠(yuǎn)時(shí)觸發(fā)觸底事件 }; }, created() { this.getData();// 請(qǐng)求數(shù)據(jù) }, activited() { this.$nextTick(()=>{ // 定位到之前的高度 this.$refs.container.scrollTop = this.scrollTop }) }, mounted() { this.canShowNum(); // 獲取可見區(qū)域能展示多少條數(shù)據(jù) window.onresize = this.canShowNum; // 監(jiān)聽窗口變化,需要重新計(jì)算一屏能展示多少條數(shù)據(jù) window.onorientationchange = this.canShowNum; // 監(jiān)聽窗口翻轉(zhuǎn) }, computed: { // 渲染元素最后的index endIndex() { let end = this.startIndex + this.showNum * 3; // 3倍是需要預(yù)留緩沖區(qū)域 let len = this.allList.length return end >= len ? len : end; // 結(jié)束元素大于所有元素的長度時(shí),就取元素長度 }, // 需要渲染的數(shù)據(jù) showList() { return this.allList.slice(this.startIndex, this.endIndex) }, // 空白占位的高度 paddingStyle() { return { paddingTop: this.startIndex * this.oneHeight + 'px', paddingBottom: (this.allList.length - this.endIndex) * this.oneHeight + 'px' } } }, methods: { // 請(qǐng)求數(shù)據(jù) getData() { this.isRequest = true // 正在請(qǐng)求中 axios.get("http://localhost:4000/data?num=10").then((res) => { // 將結(jié)果追加到allList this.allList = [...this.allList, ...res.data.list]; this.isRequest = false }); }, // 計(jì)算可見區(qū)域能展示的條數(shù) canShowNum() { // ~~ 按位兩次取反,得到整數(shù) this.showNum = ~~(this.$refs.container.offsetHeight / this.oneHeight) + 2; }, // 監(jiān)聽滾動(dòng) handleScroll(e) { if (this.canScroll) { this.canScroll = false // 處理數(shù)據(jù) this.handleData(e) // 節(jié)流 let timer = setTimeout(() => { this.canScroll = true clearTimeout(timer) timer = null }, 30) } }, handleData(e) { // 記錄當(dāng)前元素滾動(dòng)的高度 this.scrollTop = e.target.scrollTop // 可見區(qū)域第一個(gè)元素的index const curIndex = ~~(e.target.scrollTop / this.oneHeight) // 渲染區(qū)域第一個(gè)元素的index,這里緩沖區(qū)域的列表?xiàng)l數(shù)使用的是this.showNum this.startIndex = curIndex < this.showNum ? 0 : curIndex - this.showNum // 滾動(dòng)距離底部,還有this.lower距離時(shí),觸發(fā)觸底事件,正在請(qǐng)求中不發(fā)送數(shù)據(jù) if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - this.lower && !this.isRequest) { this.getData() } } }, }; </script> <style> #app { height: 100vh; } .container { height: 100%; /* 設(shè)置overflow為auto才能監(jiān)聽scroll滾動(dòng)事件 */ overflow: auto; } .box { width: 96vw; height: 150px; background: #eee; border: 2px navajowhite solid; box-sizing: border-box; } </style>
模擬數(shù)據(jù)的后端代碼
- 這是本次用于模擬后端數(shù)據(jù)的代碼,采用
mock
和express
。
const Mock = require('mockjs') const express = require('express') const app = express() let sum = 1 // mock的ID // 根據(jù)入?yún)⑸蒼um條模擬數(shù)據(jù) function generatorList(num) { return Mock.mock({ [`list|${num}`]: [ { 'id|+1': sum, title: "@ctitle(15,25)", from: "@ctitle(3,10)", } ] }) } // 允許跨域 app.all('*', function (req, res, next) { res.setHeader("Access-Control-Allow-Origin", '*'); res.setHeader("Access-Control-Allow-Headers", '*'); res.setHeader("Access-Control-Allow-Method", '*'); next() }) app.get('/data', function (req, res) { const { num } = req.query const data = generatorList(num) sum += parseInt(num) return res.send(data) }) const server = app.listen(4000, function () { console.log('4000端口正在監(jiān)聽~~') })
四、封裝為組件
也可以封裝為插件,此處為了方便就封裝為組件
props:
- allList : 所有數(shù)據(jù)
- oneHeight : 單條元素的高度
- lower : 距離底部多遠(yuǎn)時(shí)觸發(fā)觸底事件,默認(rèn)50
event:
- @scrollLower : 觸底時(shí)觸發(fā)
虛擬列表組件代碼
<template> <!-- 監(jiān)聽滾動(dòng)事件使用passive修飾符 --> <div class="container" ref="container" @scroll.passive="handleScroll"> <div :style="paddingStyle"> <!-- key使用index,可避免多次渲染該dom --> <div v-for="(item, index) in showList" :key="index"> <!-- 使用作用域插槽,將遍歷后的數(shù)據(jù)item和index傳遞出去 --> <slot :item="item" :$index="index"></slot> </div> <div>到低了~~~</div> </div> </div> </template> <script> export default { name: "App", props:{ // 所有數(shù)據(jù) allList:{ type:Array, default(){ return [] } }, // 單條數(shù)據(jù)的高度 oneHeight:{ type:Number, default:0 }, // 距離底部多遠(yuǎn)時(shí)觸發(fā)觸底事件 lower:{ type:Number, default:50 } }, data() { return { showNum: 0, // 可見區(qū)域最多能展示多少條數(shù)據(jù) startIndex: 0, // 渲染元素的第一個(gè)索引 canScroll: true, // 可以監(jiān)聽滾動(dòng),用于節(jié)流 scrollTop: 0,// 當(dāng)前滾動(dòng)高度,再次返回頁面時(shí)能定位到之前的滾動(dòng)高度 }; }, activited() { this.$nextTick(()=>{ // 定位到之前的高度 this.$refs.container.scrollTop = this.scrollTop }) }, mounted() { this.canShowNum(); // 獲取可見區(qū)域能展示多少條數(shù)據(jù) window.onresize = this.canShowNum; // 監(jiān)聽窗口變化,需要重新計(jì)算一屏能展示多少條數(shù)據(jù) window.onorientationchange = this.canShowNum; // 監(jiān)聽窗口翻轉(zhuǎn) }, computed: { // 渲染元素最后的index endIndex() { let end = this.startIndex + this.showNum * 3; // 3倍是需要預(yù)留緩沖區(qū)域 let len = this.allList.length return end >= len ? len : end; // 結(jié)束元素大于所有元素的長度時(shí),就取元素長度 }, // 需要渲染的數(shù)據(jù) showList() { return this.allList.slice(this.startIndex, this.endIndex) }, // 空白占位的高度 paddingStyle() { return { paddingTop: this.startIndex * this.oneHeight + 'px', paddingBottom: (this.allList.length - this.endIndex) * this.oneHeight + 'px' } } }, methods: { // 計(jì)算可見區(qū)域能展示的條數(shù) canShowNum() { // ~~ 按位兩次取反,得到整數(shù) this.showNum = ~~(this.$refs.container.offsetHeight / this.oneHeight) + 2; }, // 監(jiān)聽滾動(dòng) handleScroll(e) { if (this.canScroll) { this.canScroll = false // 處理數(shù)據(jù) this.handleData(e) // 節(jié)流 let timer = setTimeout(() => { this.canScroll = true clearTimeout(timer) timer = null }, 30) } }, handleData(e) { // 記錄當(dāng)前元素滾動(dòng)的高度 this.scrollTop = e.target.scrollTop // 可見區(qū)域第一個(gè)元素的index const curIndex = ~~(e.target.scrollTop / this.oneHeight) // 渲染區(qū)域第一個(gè)元素的index,這里緩沖區(qū)域的列表?xiàng)l數(shù)使用的是this.showNum this.startIndex = curIndex < this.showNum ? 0 : curIndex - this.showNum // 滾動(dòng)距離底部,還有this.lower距離時(shí),觸發(fā)觸底事件,正在請(qǐng)求中不發(fā)送數(shù)據(jù) if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - this.lower) { this.$emit('scrollLower') // 傳遞觸底事件 } } }, }; </script> <style> .container { height: 100%; /* 設(shè)置overflow為auto才能監(jiān)聽scroll滾動(dòng)事件 */ overflow: auto; } </style>
使用代碼
<template> <div id="app"> <VScroll :allList="allList" :oneHeight="150" :lower="150" @scrollLower="scrollLower"> <!-- 作用域插槽,使用slot-scope取出在組件中遍歷的數(shù)據(jù) --> <template slot-scope="{item}"> <div class="box"> <h2>{{ item.title }} - {{ item.id }}</h2> <h3>{{ item.from }}</h3> </div> </template> </VScroll> </div> </template> <script> import axios from "axios"; import VScroll from "./components/VScroll.vue"; export default { name: "App", data() { return { allList: [], // 所有數(shù)據(jù) isRequest: false // 是否正在請(qǐng)求數(shù)據(jù) }; }, created() { this.getData(); // 請(qǐng)求數(shù)據(jù) }, methods: { // 請(qǐng)求數(shù)據(jù) getData() { this.isRequest = true; // 正在請(qǐng)求中 axios.get("http://localhost:4000/data?num=10").then((res) => { // 將結(jié)果追加到allList this.allList = [...this.allList, ...res.data.list]; this.isRequest = false; }); }, // 滾動(dòng)到底部 scrollLower() { if (!this.isRequest) this.getData() } }, components: { VScroll } }; </script> <style> #app { height: 100vh; } .box { width: 96vw; height: 150px; background: #eee; border: 2px navajowhite solid; box-sizing: border-box; } </style>
到此這篇關(guān)于在vue中實(shí)現(xiàn)虛擬列表組件,解決長列表性能問題的文章就介紹到這了,更多相關(guān)vue虛擬列表組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue element 關(guān)閉當(dāng)前tab 跳轉(zhuǎn)到上一路由操作
這篇文章主要介紹了vue element 關(guān)閉當(dāng)前tab 跳轉(zhuǎn)到上一路由操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07Vue數(shù)據(jù)更新頁面卻沒有更新的幾種情況以及解決方法
我們?cè)陂_發(fā)過程中會(huì)碰到數(shù)據(jù)更新,但是頁面卻沒有更新的情況,下面這篇文章主要給大家介紹了關(guān)于Vue數(shù)據(jù)更新頁面卻沒有更新的幾種情況以及解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06解決elementui上傳組件el-upload無法第二次上傳問題
這篇文章主要介紹了解決elementui上傳組件el-upload無法第二次上傳問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03axios發(fā)送post請(qǐng)求springMVC接收不到參數(shù)的解決方法
下面小編就為大家分享一篇axios發(fā)送post請(qǐng)求springMVC接收不到參數(shù)的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03Vue.js分頁組件實(shí)現(xiàn):diVuePagination的使用詳解
這篇文章主要介紹了Vue.js分頁組件實(shí)現(xiàn):diVuePagination的使用詳解,需要的朋友可以參考下2018-01-01基于Vue3實(shí)現(xiàn)的圖片散落效果實(shí)例
最近工作中遇到一個(gè)效果還不錯(cuò),所以想著實(shí)現(xiàn)一下,下面這篇文章主要給大家介紹了關(guān)于如何基于Vue3實(shí)現(xiàn)的圖片散落效果的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04