基于Vue3與免費(fèi)滿血版DeepSeek實(shí)現(xiàn)無限滾動(dòng)+懶加載+瀑布流模塊及優(yōu)化過程
一、前言
在進(jìn)行非完全標(biāo)準(zhǔn)化數(shù)據(jù)的可視化展示時(shí),瀑布流是一種經(jīng)常被采用的展示方法。瀑布流能夠有效地將不同大小規(guī)格的內(nèi)容以一種相對(duì)規(guī)整的方式呈現(xiàn)出來,尤其在處理海量數(shù)據(jù)時(shí),依然能夠保持出色的展示效果,給人一種雜而不亂、亂中有序的積極感受。
舉幾個(gè)例子,像小紅書、淘寶、京東、千圖網(wǎng)等平臺(tái),都采用了這種布局方式。固定列數(shù)、大量元素、每個(gè)元素高度各不相同的情況,就是我們所說的瀑布流布局。
實(shí)際在開發(fā)中,瀑布流離不開的一個(gè)情況就是海量數(shù)據(jù),那么應(yīng)對(duì)海量數(shù)據(jù)最好的設(shè)計(jì)模式是加入懶加載和無限滾動(dòng),但是做無限滾動(dòng)還要同時(shí)做好頁面的優(yōu)化(即DOM的產(chǎn)生、銷毀與復(fù)現(xiàn)策略),否則在滾動(dòng)的過程中頁面DOM不斷堆砌,越來越多,會(huì)導(dǎo)致內(nèi)存泄漏,嚴(yán)重的時(shí)候會(huì)導(dǎo)致頁面崩潰。
以前想要實(shí)現(xiàn)這些內(nèi)容非常麻煩,現(xiàn)在我們可以使用騰訊云提供的免費(fèi)滿血版deepseek來快速搭建一個(gè)無限滾動(dòng)+懶加載+瀑布流的模塊,用到即賺到。
二、如何使用騰訊云免費(fèi)滿血版deepseek
1、騰訊云大模型知識(shí)引擎體驗(yàn)中心
進(jìn)入騰訊云大模型知識(shí)引擎體驗(yàn)中心:https://lke.cloud.tencent.com/lke#/experience-center/home
沒注冊(cè)賬號(hào)的注冊(cè)一下,我這里是已經(jīng)注冊(cè)后的效果。
2、體驗(yàn)deepseek聯(lián)網(wǎng)助手
找到deepseek聯(lián)網(wǎng)助手,點(diǎn)擊立即體驗(yàn):
3、人機(jī)交互獲取AI支持
這里我們問一下瀑布流是什么來測(cè)試,看看deepseek-R1模型提供的回答。
三、基于DeepSeek實(shí)現(xiàn)無限滾動(dòng)+懶加載+瀑布流模塊
1、無限滾動(dòng)+懶加載+瀑布流模塊的底層邏輯
在正式提問之前,我們要先做好頂層設(shè)計(jì)。請(qǐng)注意:AI工具只能當(dāng)做顧問,不能當(dāng)做專家。當(dāng)你無法理解AI,無法駕馭AI的時(shí)候,請(qǐng)先慢下來,專注于自身思維的提升。
首先,我們要搞清楚這個(gè)模塊實(shí)現(xiàn)的難點(diǎn)在哪:
①新元素的加載時(shí)機(jī):新元素什么時(shí)候加載?那肯定是某一列上一個(gè)元素尾部到達(dá)某個(gè)界限的時(shí)候,這個(gè)界限可以是視口的最底部,也可以是視口底部再往下一個(gè)固定數(shù)值(比如視口底部往下1000px,這樣是為了提前加載圖片內(nèi)容優(yōu)化用戶體驗(yàn))
②無限滾動(dòng)帶來的內(nèi)存泄漏問題:在元素不斷加載的過程中,頁面中累積的元素會(huì)越來越多,造成的內(nèi)存泄露問題也越來越大, 直至頁面崩潰,所以很多網(wǎng)站為了避免這個(gè)問題,在做了瀑布流的同時(shí)拒絕實(shí)現(xiàn)無限滾動(dòng)。那要怎么避免內(nèi)存泄漏的問題呢?我們可以記錄一個(gè)位置,比如視口上方1000px,當(dāng)元素的頂部隨著滾動(dòng)到達(dá)視口上方1000px位置的時(shí)候,就記錄該元素的元數(shù)據(jù)信息,然后銷毀該DOM。這樣就能避免DOM元素?zé)o限堆砌,同時(shí)記錄的元數(shù)據(jù)信息(id、url、標(biāo)題等重要數(shù)據(jù))可以在用戶回滾的時(shí)候重現(xiàn)這些元素。
也就是說,我們要:
基于DOM元素與視口的空間關(guān)系來判斷新元素添加、銷毀與重建的時(shí)機(jī),同時(shí)監(jiān)測(cè)元素與視口頂部的距離,超過預(yù)定閾值,就記錄元素元數(shù)據(jù)并銷毀DOM(這是為了在用戶回滾的時(shí)候依然能按順序生成銷毀的DOM元素),以避免內(nèi)存泄漏。
2、人機(jī)交互策略與Deepseek的實(shí)現(xiàn)過程
先觀察思考過程判斷Deepseek是否跑偏,是否按照我們的頂層設(shè)計(jì)來實(shí)現(xiàn)需求。
再來分析deepseek提供的設(shè)計(jì)思路:
①虛擬列表管理
維護(hù)兩個(gè)數(shù)據(jù)池:
- virtualPool:存儲(chǔ)所有已加載的元數(shù)據(jù)(包括尺寸、位置等信息)
- visibleItems:當(dāng)前視口內(nèi)實(shí)際渲染的DOM元素集合
interface VirtualItem { id: string data: any height: number top: number column: number } const virtualPool = ref<VirtualItem[]>([]) const visibleItems = ref<Set<string>>(new Set())
②布局容器初始化
使用CSS Grid創(chuàng)建自適應(yīng)列布局,通過ResizeObserver動(dòng)態(tài)調(diào)整列數(shù):
<template> <div class="waterfall-container" ref="container"> <div v-for="col in columns" :key="col" class="column" /> </div> </template> <script setup> const container = ref<HTMLElement>() const columns = ref(3) onMounted(() => { const observer = new ResizeObserver(entries => { columns.value = Math.floor(entries[0].contentRect.width / 300) }) observer.observe(container.value) }) </script>
③動(dòng)態(tài)渲染與銷毀機(jī)制
Intersection Observer配置,創(chuàng)建雙閾值觀察器(頂部/底部):
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const id = entry.target.dataset.id if (entry.isIntersecting) { visibleItems.value.add(id) } else { if (entry.boundingClientRect.top < entry.rootBounds!.top) { // 元素離開視口頂部時(shí)記錄位置并銷毀 recordPosition(id) visibleItems.value.delete(id) } } }) }, { root: null, rootMargin: '200px 0px', threshold: 0.1 })
DOM生命周期管理,通過動(dòng)態(tài)組件實(shí)現(xiàn)條件渲染:
<template v-for="col in columns" :key="col" class="column"> <div v-for="item in getColumnItems(col)" :key="item.id"> <component :is="visibleItems.has(item.id) ? 'RealComponent' : 'Placeholder'" :data-id="item.id" :data="item.data" /> </div> </template>
④無線滾動(dòng)實(shí)現(xiàn)
滾動(dòng)事件節(jié)流處理使用 requestAnimationFrame 優(yōu)化性能:
const checkScroll = () => { const { scrollTop, scrollHeight, clientHeight } = document.documentElement if (scrollHeight - (scrollTop + clientHeight) < 500) { loadMoreItems() } } window.addEventListener('scroll', () => { requestAnimationFrame(checkScroll) })
數(shù)據(jù)分頁加載:
const loadMoreItems = async () => { const lastItem = virtualPool.value[virtualPool.value.length - 1] const newItems = await fetchItems({ offset: virtualPool.value.length, position: lastItem?.top || 0 }) newItems.forEach(item => { virtualPool.value.push({ ...item, height: 0, // 初始占位高度 top: calculateInitialPosition(item) }) }) }
⑤內(nèi)存優(yōu)化策略
使用Map結(jié)構(gòu)存儲(chǔ)已銷毀元素的位置信息:
const positionCache = new Map<string, { top: number, column: number }>() const recordPosition = (id: string) => { const element = document.querySelector(`[data-id="${id}"]`) if (element) { positionCache.set(id, { top: element.getBoundingClientRect().top + window.scrollY, column: parseInt(element.parentElement!.dataset.column!) }) } }
基于歷史數(shù)據(jù)預(yù)測(cè)未加載元素的高度:
const predictHeight = (dataType: string) => { const history = virtualPool.value .filter(item => item.data.type === dataType) .map(item => item.height) return history.length > 0 ? Math.ceil(history.reduce((a,b) => a+b)/history.length) : 200 // 默認(rèn)高度 }
如果還有更多需求,繼續(xù)人機(jī)交互完善代碼即可,核心是我提供的思路+強(qiáng)力的AI工具,剩下的就是時(shí)間問題。
四、最終代碼呈現(xiàn)
1、組件代碼
<template> <div class="masonry-container" ref="container"> <div v-for="(col, index) in columns" :key="index" class="masonry-column" :style="{ width: columnWidth + 'px', marginRight: gutter + 'px' }"> <div v-for="item in columnItems[index]" :key="item._uid" :class="itemClass" :style="{ transition: `all ${transitionDuration}ms ease`, transform: `translateY(${item.y}px)` }" transitionend="handleTransitionEnd(item)"> <slot name="item" :item="item.data"></slot> </div> </div> </div> </template> <script> import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue' export default { props: { items: { type: Array, required: true }, columns: { type: Number, default: 2 }, gutter: { type: Number, default: 8 }, breakpoints: { type: Object, default: () => ({}) }, resizeObserver: { type: Boolean, default: true }, useImageLoader: { type: Boolean, default: true }, itemClass: { type: String, default: 'masonry-item' }, transitionDuration: { type: Number, default: 300 } }, emits: ['layout-complete', 'item-positioned'], setup(props, { emit }) { const container = ref(null) const columnHeights = ref([]) const columnItems = ref([]) const activeColumns = ref(props.columns) const observer = ref(null) // 計(jì)算列寬 const columnWidth = computed(() => { if (!container.value) return 0 const totalGutter = (activeColumns.value - 1) * props.gutter return (container.value.offsetWidth - totalGutter) / activeColumns.value }) // 響應(yīng)式斷點(diǎn)處理 const handleBreakpoints = () => { const breakpoints = Object.entries(props.breakpoints) .sort((a, b) => b[0] - a[0]) const width = window.innerWidth for (const [bp, cols] of breakpoints) { if (width >= bp) { activeColumns.value = cols return } } activeColumns.value = props.columns } // 布局核心算法 const layoutItems = () => { columnHeights.value = new Array(activeColumns.value).fill(0) columnItems.value = new Array(activeColumns.value).fill([]) props.items.forEach(item => { const minHeight = Math.min(...columnHeights.value) const columnIndex = columnHeights.value.indexOf(minHeight) const newItem = { ...item, y: minHeight, _uid: Math.random().toString(36).substr(2, 9) } columnItems.value[columnIndex] = [ ...columnItems.value[columnIndex], newItem ] // 觸發(fā)單個(gè)元素定位事件 emit('item-positioned', { element: newItem, position: { x: columnIndex * (columnWidth.value + props.gutter), y: minHeight } }) // 更新列高度(假設(shè)已獲取元素高度) columnHeights.value[columnIndex] += item._height + props.gutter }) // 觸發(fā)布局完成事件 emit('layout-complete', { columns: activeColumns.value, containerHeight: Math.max(...columnHeights.value) }) } // 圖片加載處理 const loadImages = () => { if (!props.useImageLoader) return props.items.forEach(item => { const img = new Image() img.src = item.image img.onload = () => { item._height = (columnWidth.value * img.height) / img.width layoutItems() } }) } // 初始化 onMounted(() => { handleBreakpoints() loadImages() layoutItems() if (props.resizeObserver) { observer.value = new ResizeObserver(() => { handleBreakpoints() layoutItems() }) observer.value.observe(container.value) } window.addEventListener('resize', handleBreakpoints) }) onBeforeUnmount(() => { if (observer.value) observer.value.disconnect() window.removeEventListener('resize', handleBreakpoints) }) // 監(jiān)聽相關(guān)變化 watch(() => props.items, layoutItems) watch(activeColumns, layoutItems) return { container, columnWidth, columnItems } } } </script> <style> .masonry-container { position: relative; display: flex; justify-content: flex-start; } .masonry-column { transition: all 0.3s ease; } .masonry-item { position: absolute; width: 100%; will-change: transform; } </style>
2、組件用法
<template> <MasonryLayout :items="items" @layout-complete="handleLayoutComplete" > </MasonryLayout> </template> <script setup> import { onMounted } from 'vue' const handleLayoutComplete = ({ columns, containerHeight }) => { console.log(`當(dāng)前列數(shù):${columns},容器高度:${containerHeight}px`) } // 滾動(dòng)加載更多 window.addEventListener('scroll', () => { if (nearBottom()) { items.value = [...items.value, ...newItems] } }) </script>
五、結(jié)語
瀑布流+無限滾動(dòng)+懶加載的結(jié)合能夠提升用戶體驗(yàn)和頁面性能:瀑布流以錯(cuò)落有致的布局呈現(xiàn)內(nèi)容,增加視覺吸引力;無限滾動(dòng)讓用戶無需翻頁即可持續(xù)瀏覽,提高交互流暢性;懶加載則延遲加載非可視區(qū)域的內(nèi)容,減少初始加載時(shí)間與資源消耗,從而實(shí)現(xiàn)高效、動(dòng)態(tài)且美觀的內(nèi)容展示。
以上就是基于Vue3與免費(fèi)滿血版DeepSeek實(shí)現(xiàn)無限滾動(dòng)+懶加載+瀑布流模塊及優(yōu)化過程的詳細(xì)內(nèi)容,更多關(guān)于Vue3 DeepSeek無限滾動(dòng)+懶加載+瀑布流模塊的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文教會(huì)你搭建vite項(xiàng)目并配置路由和element-plus
由于項(xiàng)目搭建過程實(shí)在繁瑣,容易遺忘,每次新建項(xiàng)目還得百度一下怎么搭建,所以寫下本文提醒自己,下面這篇文章主要給大家介紹了關(guān)于搭建vite項(xiàng)目并配置路由和element-plus的相關(guān)資料,需要的朋友可以參考下2022-07-07vue實(shí)現(xiàn)大文件分片上傳與斷點(diǎn)續(xù)傳到七牛云
這篇文章介紹了vue實(shí)現(xiàn)大文件分片上傳與斷點(diǎn)續(xù)傳到七牛云的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06