Vue3封裝自動(dòng)滾動(dòng)列表指令(含網(wǎng)頁縮放滾動(dòng)問題)
長(zhǎng)列表自動(dòng)滾動(dòng)跟banner自動(dòng)切換一樣,是一個(gè)C端展示頁面經(jīng)常遇到的需求。不過網(wǎng)上各類組件庫基本都對(duì)“幻燈片”(banner使用的組件)組件有封裝,自帶好自動(dòng)切換的功能,而長(zhǎng)列表(或表格等)自動(dòng)滾動(dòng)的功能則涉及甚少。
這里分享一個(gè)我項(xiàng)目中封裝的自動(dòng)滾動(dòng)指令,且附帶期間解決了頁面縮放導(dǎo)致滾動(dòng)失效的解決思路與方案:
需求定義
首先,我們需要理清這個(gè)通用指令的需求點(diǎn),方便進(jìn)行下一步設(shè)計(jì)。這里需要的需求點(diǎn)有:
- 支持任意組件/標(biāo)簽的滾動(dòng)。
- 可控制滾動(dòng)速度。
- 可設(shè)置鼠標(biāo)移入后停止?jié)L動(dòng)。
大致思路設(shè)計(jì)
首先,滿足第一點(diǎn),即指令可以在任意組件(包括原生的與自己封裝的Vue組件)上使用,考慮到滾動(dòng)相關(guān)的API,初步確定,我們需要使用到:
scrollTo
, scrollHeight
, clientHeight
分別用來設(shè)置滾動(dòng)高度,獲取最大滾動(dòng)高度 與 可視區(qū)域高度。
這一點(diǎn)很好滿足,任何通過我們上述的組件獲取到的DOM(HTMLElement)都支持這三個(gè)接口/屬性。
其次是速度控制,使用數(shù)字來控制每幀滾動(dòng)的距離(px),我們可以將速度作為指令的參數(shù)。
最后是可選的鼠標(biāo)移入暫停功能,boolean類型的選項(xiàng),很容易就能想到使用指令修飾符。
綜上,我們的指令調(diào)用方式需要滿足以下:
<element v-scroll.mouse="1.5"></element>
編碼實(shí)現(xiàn)
1. 指令注冊(cè)和整體“架構(gòu)”代碼
/** * main.ts */ import scrollDirective from '@/directive/scroll.ts'; ... const app = createApp(App); // 注冊(cè)指令 app.directive(scrollDirective.name, scrollDirective); ...
/** * scroll.ts */ import { DirectiveBinding } from 'vue'; export default { name: 'scroll', mounted: (el: HTMLElement, binding: DirectiveBinding) => {}, unmounted: (el: HTMLElement, binding: DirectiveBinding) => {} }
2. 速度控制以及滾動(dòng)
JS編寫動(dòng)畫,本能的就想到RAF
能夠最好的實(shí)現(xiàn)動(dòng)畫效果,RAF
并在性能與視覺效果之間自動(dòng)做好權(quán)衡,就是你了。
將速度speed
作為指令參數(shù),在動(dòng)畫函數(shù)中,用當(dāng)前 scrollTop
累加 speed
來實(shí)現(xiàn)滾動(dòng)效果。
/** * scroll.ts */ import { DirectiveBinding } from 'vue'; // 我們會(huì)在DOM上拓展一些屬性用于滾動(dòng)動(dòng)畫的執(zhí)行,這里拓展一下類型,方便編碼 interface AnimationElement extends HTMLElement { speed: number; } const DEFAULT_SPEED = 1; const RAF = window.requestAnimationFrame; const CancelRAF = window.cancelAnimationFrame; const elementScroll = (el: AnimationElement) => { const maxScrollTop = el.scrollHeight - el.clientHeight; // 根據(jù)當(dāng)前滾動(dòng)高度與滾動(dòng)速度,計(jì)算出新的滾動(dòng)高度 const scrollTop = el.scrollTop + el.speed >= maxScrollTop // 超過最大滾動(dòng)高度重置 --〉 從頭再來 ? 0 : el.scrollTop + el.speed; // 執(zhí)行滾動(dòng) el.scrollTo({ top: scrollTop }); // 繼續(xù)執(zhí)行下一幀動(dòng)畫 RAF(elementScroll.bind(null, el)); } export default { name: 'scroll', mounted: (el: AnimationElement, binding: DirectiveBinding) => { const maxScrollTop = el.scrollHeight - el.clientHeight; // 沒有滾動(dòng)空間的時(shí)候,無需滾動(dòng),直接返回。 if (maxScrollTop <= 0) { return; } // 將速度變量存到DOM中,方便后續(xù)動(dòng)畫函數(shù)取用 el.speed = binding.value || DEFAULT_SPEED; // 使用RAF調(diào)用動(dòng)畫函數(shù) RAF(elementScroll.bind(null, el)); }, unmounted: (el: HTMLElement, binding: DirectiveBinding) => {} }
我們隨便寫一個(gè)列表來試試
<template> <ul v-scroll class="w-[300px] h-[400px] overflow-auto bg-[darkcyan]"> <li>1</li> <li>2</li> ... </ul> </template>
看看效果,perfect!
可以修改一下速度,讓他滾快點(diǎn)兒
<ul v-scroll="2" ...>
Nice,沒毛病。
3. 鼠標(biāo)移入暫停滾動(dòng),移出恢復(fù)滾動(dòng)
要實(shí)現(xiàn)這個(gè)功能有兩個(gè)要點(diǎn):
一是事件監(jiān)聽,鼠標(biāo)移入/移出容器時(shí),將動(dòng)畫暫停/重啟;
二是獲取到當(dāng)前容器滾動(dòng)動(dòng)畫id(RAF
返回的),鼠標(biāo)移入時(shí),使用 window.cancelAnimationFrame
暫停動(dòng)畫。
/** * scroll.ts */ interface AnimationElement extends HTMLElement { speed: number; animationId: number; } ... const elementScroll = (el: AnimationElement) => { ... // 繼續(xù)執(zhí)行下一幀,并更新動(dòng)畫id el.animationId = RAF(elementScroll.bind(null, el)); } // 鼠標(biāo)移入暫停 const mouseEnterHandler = (el: AnimationElement) => { if (el.animationId) { // 取消動(dòng)畫 CancelRAF(el.animationId); el.animationId = undefined; } }; // 鼠標(biāo)移出繼續(xù)運(yùn)行動(dòng)畫 const mouseLeaveHandler = (el: AnimationElement) => (el.animationId = RAF(scrollElement.bind(null, el))); export default { name: 'scroll', mounted: (el: AnimationElement, binding: DirectiveBinding) => { ... // 修改原來初始化運(yùn)行動(dòng)畫的語句,將RAF結(jié)果存到el中,方便暫停動(dòng)畫時(shí)使用 el.animationId = RAF(scrollElement.bind(null, el)); // 檢測(cè)是否傳遞修飾符,傳遞了監(jiān)聽鼠標(biāo)移入移出動(dòng)畫 if (binding.modifiers.mouse) { el.addEventListener('mouseenter', mouseEnterHandler.bind(null, el)); el.addEventListener('mouseleave', mouseLeaveHandler.bind(null, el)); } }, unmounted: (el: HTMLElement, binding: DirectiveBinding) => { // 別忘了DOM解綁時(shí)解除事件監(jiān)聽 if (binding.modifiers.mouse) { el.removeEventListener( 'mouseenter', mouseEnterHandler.bind(null, el) ); el.removeEventListener( 'mouseleave', mouseLeaveHandler.bind(null, el) ); } } }
淺寫個(gè)demo看看效果
<template> <ul v-scroll.mouse class="w-[300px] h-[400px] overflow-auto bg-[darkcyan]"> <li>1</li> <li>2</li> ... </ul> </template>
make scroll perfect again!
這樣我們就實(shí)現(xiàn)了一個(gè)可以控制滾動(dòng)速度,支持鼠標(biāo)移入暫停滾動(dòng)的通用滾動(dòng)指令了。
存在問題
第一版就這樣上線使用了,但很快哈,啪的一下,我就發(fā)現(xiàn)了一些問題:
- 傳入小數(shù)時(shí),列表不滾動(dòng)。
- 頁面縮放后,列表不滾動(dòng)。
1. 問題原因探究
首先要想解決問題,在不存在魔法的情況下,我們要先尋找問題的原因。
既然小數(shù)速度無法滾動(dòng),那我們?cè)跒g覽器上測(cè)試一二
讓頁面向下滾動(dòng) 0.7
, 結(jié)果發(fā)現(xiàn)還是 0
(⊙o⊙),所以我們下次累加的時(shí)候還是 0 + 0.5
無限循環(huán),一直是 0
。
隨后,我翻閱了一遍 W3C
文檔,找到 scrollTo
函數(shù)相關(guān)部分,不過文檔并未直接說top
參數(shù)的處理會(huì)向下去整,反而 interface ScrollOptions
中的 top
正是 double
類型,這說明他實(shí)際上是支持小數(shù)的哇,那這是為什么?
掃視了好幾遍之后,發(fā)現(xiàn)了一個(gè)頻頻出現(xiàn)的單詞 pixels
,這些參數(shù)都是以像素為單位的。
像素,像素?像素...??!道爺我悟了!
可不是嘛,這哪來的 0.5 個(gè)像素嘛,這可不得取整?
順便,在翻閱文檔時(shí),也找到了,網(wǎng)頁縮放后滾動(dòng)失效(即使speed >= 1
)的原因:W3C
文檔VisualViewport
中找到了這句話,滾動(dòng)高度會(huì)隨著頁面縮放變小。
我們?cè)?code>Chrome嘗試一下,看看是否屬實(shí):
現(xiàn)在正常大小網(wǎng)頁設(shè)置一下滾動(dòng)高度,并沒有什么問題
隨后,縮放網(wǎng)頁到90%,馬上哈,Y軸的滾動(dòng)量就變成 0
了
再嘗試一下賦值其他的值,會(huì)發(fā)現(xiàn),縮放后設(shè)置滾動(dòng)高度后,其真實(shí)的滾動(dòng)量確實(shí)減少了,但不是按照我們樸素思維等比例減少的(具體怎么個(gè)算法,沒找到...)
不過知道這點(diǎn)就足夠了,在當(dāng)前情況下,想要實(shí)現(xiàn)我們要的小數(shù)級(jí)別的滾動(dòng)速度,那么我們必然不能直接基于 el.scrollTop
來滾動(dòng)了,必須有所變通。
2. 問題解決:緩存計(jì)算
在哐哐哐一通嘗試下(css animation | 改用setTimeout,把間隔時(shí)間放長(zhǎng) | etc.),最終我想到了一個(gè)破費(fèi)科特的辦法,既能滿足我們的需求,又很簡(jiǎn)單不需要大量改動(dòng)現(xiàn)有代碼:
—— 緩存計(jì)算滾動(dòng)高度
顧名思義,即當(dāng)el.scrollTop
不可靠的時(shí)候,那么就由我們自己來手動(dòng)管理滾動(dòng)高度,設(shè)置一個(gè)自定義的變量來對(duì)scrollTop
進(jìn)行累加,這樣就規(guī)避掉了el.scrollTop
“只會(huì)取整”(并不是),導(dǎo)致設(shè)置 0.5
速度后,el.scrollTop
一直是0
無法累加的問題了。
同時(shí)由于 scrollTop
是我們自己進(jìn)行計(jì)算累加的,也不會(huì)受到網(wǎng)頁縮放的影響了,縮放后也能正常地進(jìn)行滾動(dòng)了。
這樣即使我們 speed = 0.5
也能夠正常“慢速滾動(dòng)”(本質(zhì)上非整數(shù)的幀滾動(dòng)高度相同,即達(dá)到了速度放慢的效果)
3. 修改后完整代碼
PS:需要特別注意的是,將基準(zhǔn)滾動(dòng)高度改為我們的自定義緩存滾動(dòng)高度后,用戶自行滾動(dòng)的事件是不會(huì)自動(dòng)同步到我們的緩存滾動(dòng)高度的,所以需要我們自己同步一下。
/** * 自動(dòng)滾動(dòng) * * 修飾符: * mouse 支持鼠標(biāo)移入移出暫停動(dòng)畫 */ import { DirectiveBinding } from 'vue'; interface AnimationElement extends HTMLElement { speed: number; animationId: number; cacheScrollTop: number; // 存放我們緩存的scrollTop } const RAF = window.requestAnimationFrame; const CancelRAF = window.cancelAnimationFrame; const scrollElement = (el: AnimationElement) => { const maxScrollTop = el.scrollHeight - el.clientHeight; // 直接在緩存滾動(dòng)高度上進(jìn)行計(jì)算 el.cacheScrollTop = el.cacheScrollTop + el.speed >= maxScrollTop ? 0 : el.cacheScrollTop + el.speed; // 將緩存高度設(shè)置為當(dāng)前滾動(dòng)高度 el.scrollTo({ top: el.cacheScrollTop }); // 執(zhí)行下一幀 el.animationId = RAF(scrollElement.bind(null, el)); }; // 鼠標(biāo)移入暫停 const mouseEnterHandler = (el: AnimationElement) => { if (el.animationId) { CancelRAF(el.animationId); el.animationId = undefined; } }; // 鼠標(biāo)移出繼續(xù)運(yùn)行 const mouseLeaveHandler = (el: AnimationElement) => (el.animationId = RAF(scrollElement.bind(null, el))); // 處理用戶的滾動(dòng)事件 const elementScrollHandler = (el: AnimationElement) => (el.cacheScrollTop = el.scrollTop); export default { name: 'scroll', mounted: (el: AnimationElement, binding: DirectiveBinding) => { const maxScrollTop = el.scrollHeight - el.clientHeight; // 無需滾動(dòng)(這里 - 1因?yàn)閟crollHeight會(huì)四舍五入) if (maxScrollTop - 1 <= 0) { return; } // 滾動(dòng)速度 el.speed = binding.value || 1; el.cacheScrollTop = 0; el.animationId = RAF(scrollElement.bind(null, el)); // PS:因?yàn)槲覀兪褂?cacheScrollTop 來代替 el.scrollTop 處理滾動(dòng)高度,所以這里需要同步一下用戶滾動(dòng)操作后的 scrollTop ==> 而為了保持動(dòng)畫連貫與流暢,這里千萬不要去防抖/節(jié)流! el.addEventListener('scroll', elementScrollHandler.bind(null, el)); // 鼠標(biāo)移入暫停移出繼續(xù)運(yùn)動(dòng) if (binding.modifiers.mouse) { el.addEventListener('mouseenter', mouseEnterHandler.bind(null, el)); el.addEventListener('mouseleave', mouseLeaveHandler.bind(null, el)); } }, unmounted: (el: AnimationElement, binding: DirectiveBinding) => { if (binding.modifiers.mouse) { el.removeEventListener( 'mouseenter', mouseEnterHandler.bind(null, el) ); el.removeEventListener( 'mouseleave', mouseLeaveHandler.bind(null, el) ); } } };
總結(jié)
- 使用
RAF
作為滾動(dòng)動(dòng)畫“框架” - 鼠標(biāo)移入移出動(dòng)畫暫停/恢復(fù),事件監(jiān)聽 +
cancelAnimationFrame
- 滾動(dòng)的基礎(chǔ)單位是像素(1px),正常網(wǎng)頁縮放情況下,會(huì)向下取整,所以得自行管理滾動(dòng)高度,對(duì)其緩存計(jì)算。
- 網(wǎng)頁縮放的情況下,滾動(dòng)高度會(huì)減少,同理也通過緩存計(jì)算來解決。
敢敢單單,86
行代碼我們就實(shí)現(xiàn)了一個(gè)基本完美的通用列表滾動(dòng)指令。
參考資料: W3C CSSOM View Module
到此這篇關(guān)于Vue3封裝自動(dòng)滾動(dòng)列表指令(含網(wǎng)頁縮放滾動(dòng)問題)的文章就介紹到這了,更多相關(guān)Vue3 自動(dòng)滾動(dòng)列表指令內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3.0+echarts實(shí)現(xiàn)立體柱圖
這篇文章主要為大家詳細(xì)介紹了vue3.0+echarts實(shí)現(xiàn)立體柱圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Element通過v-for循環(huán)渲染的form表單校驗(yàn)的實(shí)現(xiàn)
日常業(yè)務(wù)開發(fā)中,form表單校驗(yàn)是一個(gè)很常見的問題,本文主要介紹了Element通過v-for循環(huán)渲染的form表單校驗(yàn)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04解決echarts中橫坐標(biāo)值顯示不全(自動(dòng)隱藏)問題
這篇文章主要介紹了解決echarts中橫坐標(biāo)值顯示不全(自動(dòng)隱藏)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07vue-autoui自匹配webapi的UI控件的實(shí)現(xiàn)
這篇文章主要介紹了vue-autoui自匹配webapi的UI控件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03詳細(xì)講一講vue3下會(huì)造成響應(yīng)式丟失的情況
vue3開發(fā)過程中,綁定的響應(yīng)式數(shù)據(jù)失去了響應(yīng)式,如何解決問題呢,下面這篇文章主要給大家介紹了關(guān)于vue3下會(huì)造成響應(yīng)式丟失的情況,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06Vue應(yīng)用中使用xlsx庫實(shí)現(xiàn)Excel文件導(dǎo)出的詳細(xì)步驟
本文詳細(xì)介紹了如何在Vue應(yīng)用中使用xlsx庫來導(dǎo)出Excel文件,包括安裝xlsx庫、準(zhǔn)備數(shù)據(jù)、創(chuàng)建導(dǎo)出方法、觸發(fā)導(dǎo)出操作和自定義Excel文件等步驟,xlsx庫提供了強(qiáng)大的API和靈活的自定義選項(xiàng),使得處理Excel文件變得簡(jiǎn)單而高效2024-10-10詳解如何配置vue-cli3.0的vue.config.js
這篇文章主要介紹了詳解如何配置vue-cli3.0的vue.config.js,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08vue動(dòng)態(tài)路由:路由參數(shù)改變,視圖不更新問題的解決
今天小編就為大家分享一篇vue動(dòng)態(tài)路由:路由參數(shù)改變,視圖不更新問題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11