Vue3封裝自動滾動列表指令(含網(wǎng)頁縮放滾動問題)
長列表自動滾動跟banner自動切換一樣,是一個C端展示頁面經(jīng)常遇到的需求。不過網(wǎng)上各類組件庫基本都對“幻燈片”(banner使用的組件)組件有封裝,自帶好自動切換的功能,而長列表(或表格等)自動滾動的功能則涉及甚少。
這里分享一個我項目中封裝的自動滾動指令,且附帶期間解決了頁面縮放導致滾動失效的解決思路與方案:
需求定義
首先,我們需要理清這個通用指令的需求點,方便進行下一步設(shè)計。這里需要的需求點有:
- 支持任意組件/標簽的滾動。
- 可控制滾動速度。
- 可設(shè)置鼠標移入后停止?jié)L動。
大致思路設(shè)計
首先,滿足第一點,即指令可以在任意組件(包括原生的與自己封裝的Vue組件)上使用,考慮到滾動相關(guān)的API,初步確定,我們需要使用到:
scrollTo, scrollHeight, clientHeight
分別用來設(shè)置滾動高度,獲取最大滾動高度 與 可視區(qū)域高度。
這一點很好滿足,任何通過我們上述的組件獲取到的DOM(HTMLElement)都支持這三個接口/屬性。
其次是速度控制,使用數(shù)字來控制每幀滾動的距離(px),我們可以將速度作為指令的參數(shù)。
最后是可選的鼠標移入暫停功能,boolean類型的選項,很容易就能想到使用指令修飾符。
綜上,我們的指令調(diào)用方式需要滿足以下:
<element v-scroll.mouse="1.5"></element>
編碼實現(xiàn)
1. 指令注冊和整體“架構(gòu)”代碼
/** * main.ts */ import scrollDirective from '@/directive/scroll.ts'; ... const app = createApp(App); // 注冊指令 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. 速度控制以及滾動
JS編寫動畫,本能的就想到RAF能夠最好的實現(xiàn)動畫效果,RAF并在性能與視覺效果之間自動做好權(quán)衡,就是你了。
將速度speed作為指令參數(shù),在動畫函數(shù)中,用當前 scrollTop 累加 speed 來實現(xiàn)滾動效果。
/**
* scroll.ts
*/
import { DirectiveBinding } from 'vue';
// 我們會在DOM上拓展一些屬性用于滾動動畫的執(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ù)當前滾動高度與滾動速度,計算出新的滾動高度
const scrollTop =
el.scrollTop + el.speed >= maxScrollTop // 超過最大滾動高度重置 --〉 從頭再來
? 0
: el.scrollTop + el.speed;
// 執(zhí)行滾動
el.scrollTo({
top: scrollTop
});
// 繼續(xù)執(zhí)行下一幀動畫
RAF(elementScroll.bind(null, el));
}
export default {
name: 'scroll',
mounted: (el: AnimationElement, binding: DirectiveBinding) => {
const maxScrollTop = el.scrollHeight - el.clientHeight;
// 沒有滾動空間的時候,無需滾動,直接返回。
if (maxScrollTop <= 0) {
return;
}
// 將速度變量存到DOM中,方便后續(xù)動畫函數(shù)取用
el.speed = binding.value || DEFAULT_SPEED;
// 使用RAF調(diào)用動畫函數(shù)
RAF(elementScroll.bind(null, el));
},
unmounted: (el: HTMLElement, binding: DirectiveBinding) => {}
}我們隨便寫一個列表來試試
<template>
<ul v-scroll class="w-[300px] h-[400px] overflow-auto bg-[darkcyan]">
<li>1</li>
<li>2</li>
...
</ul>
</template>看看效果,perfect!

可以修改一下速度,讓他滾快點兒
<ul v-scroll="2" ...>
Nice,沒毛病。

3. 鼠標移入暫停滾動,移出恢復(fù)滾動
要實現(xiàn)這個功能有兩個要點:
一是事件監(jiān)聽,鼠標移入/移出容器時,將動畫暫停/重啟;
二是獲取到當前容器滾動動畫id(RAF 返回的),鼠標移入時,使用 window.cancelAnimationFrame 暫停動畫。
/**
* scroll.ts
*/
interface AnimationElement extends HTMLElement {
speed: number;
animationId: number;
}
...
const elementScroll = (el: AnimationElement) => {
...
// 繼續(xù)執(zhí)行下一幀,并更新動畫id
el.animationId = RAF(elementScroll.bind(null, el));
}
// 鼠標移入暫停
const mouseEnterHandler = (el: AnimationElement) => {
if (el.animationId) {
// 取消動畫
CancelRAF(el.animationId);
el.animationId = undefined;
}
};
// 鼠標移出繼續(xù)運行動畫
const mouseLeaveHandler = (el: AnimationElement) =>
(el.animationId = RAF(scrollElement.bind(null, el)));
export default {
name: 'scroll',
mounted: (el: AnimationElement, binding: DirectiveBinding) => {
...
// 修改原來初始化運行動畫的語句,將RAF結(jié)果存到el中,方便暫停動畫時使用
el.animationId = RAF(scrollElement.bind(null, el));
// 檢測是否傳遞修飾符,傳遞了監(jiān)聽鼠標移入移出動畫
if (binding.modifiers.mouse) {
el.addEventListener('mouseenter', mouseEnterHandler.bind(null, el));
el.addEventListener('mouseleave', mouseLeaveHandler.bind(null, el));
}
},
unmounted: (el: HTMLElement, binding: DirectiveBinding) => {
// 別忘了DOM解綁時解除事件監(jiān)聽
if (binding.modifiers.mouse) {
el.removeEventListener(
'mouseenter',
mouseEnterHandler.bind(null, el)
);
el.removeEventListener(
'mouseleave',
mouseLeaveHandler.bind(null, el)
);
}
}
}淺寫個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!

這樣我們就實現(xiàn)了一個可以控制滾動速度,支持鼠標移入暫停滾動的通用滾動指令了。
存在問題
第一版就這樣上線使用了,但很快哈,啪的一下,我就發(fā)現(xiàn)了一些問題:
- 傳入小數(shù)時,列表不滾動。
- 頁面縮放后,列表不滾動。
1. 問題原因探究
首先要想解決問題,在不存在魔法的情況下,我們要先尋找問題的原因。
既然小數(shù)速度無法滾動,那我們在瀏覽器上測試一二
讓頁面向下滾動 0.7, 結(jié)果發(fā)現(xiàn)還是 0 (⊙o⊙),所以我們下次累加的時候還是 0 + 0.5 無限循環(huán),一直是 0。

隨后,我翻閱了一遍 W3C 文檔,找到 scrollTo 函數(shù)相關(guān)部分,不過文檔并未直接說top參數(shù)的處理會向下去整,反而 interface ScrollOptions 中的 top 正是 double 類型,這說明他實際上是支持小數(shù)的哇,那這是為什么?
掃視了好幾遍之后,發(fā)現(xiàn)了一個頻頻出現(xiàn)的單詞 pixels,這些參數(shù)都是以像素為單位的。
像素,像素?像素...??!道爺我悟了!
可不是嘛,這哪來的 0.5 個像素嘛,這可不得取整?
順便,在翻閱文檔時,也找到了,網(wǎng)頁縮放后滾動失效(即使speed >= 1)的原因:W3C文檔VisualViewport中找到了這句話,滾動高度會隨著頁面縮放變小。

我們在Chrome嘗試一下,看看是否屬實:
現(xiàn)在正常大小網(wǎng)頁設(shè)置一下滾動高度,并沒有什么問題

隨后,縮放網(wǎng)頁到90%,馬上哈,Y軸的滾動量就變成 0 了

再嘗試一下賦值其他的值,會發(fā)現(xiàn),縮放后設(shè)置滾動高度后,其真實的滾動量確實減少了,但不是按照我們樸素思維等比例減少的(具體怎么個算法,沒找到...)

不過知道這點就足夠了,在當前情況下,想要實現(xiàn)我們要的小數(shù)級別的滾動速度,那么我們必然不能直接基于 el.scrollTop 來滾動了,必須有所變通。
2. 問題解決:緩存計算
在哐哐哐一通嘗試下(css animation | 改用setTimeout,把間隔時間放長 | etc.),最終我想到了一個破費科特的辦法,既能滿足我們的需求,又很簡單不需要大量改動現(xiàn)有代碼:
—— 緩存計算滾動高度
顧名思義,即當el.scrollTop不可靠的時候,那么就由我們自己來手動管理滾動高度,設(shè)置一個自定義的變量來對scrollTop進行累加,這樣就規(guī)避掉了el.scrollTop“只會取整”(并不是),導致設(shè)置 0.5 速度后,el.scrollTop一直是0無法累加的問題了。
同時由于 scrollTop 是我們自己進行計算累加的,也不會受到網(wǎng)頁縮放的影響了,縮放后也能正常地進行滾動了。
這樣即使我們 speed = 0.5 也能夠正常“慢速滾動”(本質(zhì)上非整數(shù)的幀滾動高度相同,即達到了速度放慢的效果)
3. 修改后完整代碼
PS:需要特別注意的是,將基準滾動高度改為我們的自定義緩存滾動高度后,用戶自行滾動的事件是不會自動同步到我們的緩存滾動高度的,所以需要我們自己同步一下。
/**
* 自動滾動
*
* 修飾符:
* mouse 支持鼠標移入移出暫停動畫
*/
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;
// 直接在緩存滾動高度上進行計算
el.cacheScrollTop =
el.cacheScrollTop + el.speed >= maxScrollTop
? 0
: el.cacheScrollTop + el.speed;
// 將緩存高度設(shè)置為當前滾動高度
el.scrollTo({
top: el.cacheScrollTop
});
// 執(zhí)行下一幀
el.animationId = RAF(scrollElement.bind(null, el));
};
// 鼠標移入暫停
const mouseEnterHandler = (el: AnimationElement) => {
if (el.animationId) {
CancelRAF(el.animationId);
el.animationId = undefined;
}
};
// 鼠標移出繼續(xù)運行
const mouseLeaveHandler = (el: AnimationElement) =>
(el.animationId = RAF(scrollElement.bind(null, el)));
// 處理用戶的滾動事件
const elementScrollHandler = (el: AnimationElement) =>
(el.cacheScrollTop = el.scrollTop);
export default {
name: 'scroll',
mounted: (el: AnimationElement, binding: DirectiveBinding) => {
const maxScrollTop = el.scrollHeight - el.clientHeight;
// 無需滾動(這里 - 1因為scrollHeight會四舍五入)
if (maxScrollTop - 1 <= 0) {
return;
}
// 滾動速度
el.speed = binding.value || 1;
el.cacheScrollTop = 0;
el.animationId = RAF(scrollElement.bind(null, el));
// PS:因為我們使用 cacheScrollTop 來代替 el.scrollTop 處理滾動高度,所以這里需要同步一下用戶滾動操作后的 scrollTop ==> 而為了保持動畫連貫與流暢,這里千萬不要去防抖/節(jié)流!
el.addEventListener('scroll', elementScrollHandler.bind(null, el));
// 鼠標移入暫停移出繼續(xù)運動
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作為滾動動畫“框架” - 鼠標移入移出動畫暫停/恢復(fù),事件監(jiān)聽 +
cancelAnimationFrame - 滾動的基礎(chǔ)單位是像素(1px),正常網(wǎng)頁縮放情況下,會向下取整,所以得自行管理滾動高度,對其緩存計算。
- 網(wǎng)頁縮放的情況下,滾動高度會減少,同理也通過緩存計算來解決。
敢敢單單,86 行代碼我們就實現(xiàn)了一個基本完美的通用列表滾動指令。
參考資料: W3C CSSOM View Module
到此這篇關(guān)于Vue3封裝自動滾動列表指令(含網(wǎng)頁縮放滾動問題)的文章就介紹到這了,更多相關(guān)Vue3 自動滾動列表指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Element通過v-for循環(huán)渲染的form表單校驗的實現(xiàn)
日常業(yè)務(wù)開發(fā)中,form表單校驗是一個很常見的問題,本文主要介紹了Element通過v-for循環(huán)渲染的form表單校驗的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-04-04
vue-autoui自匹配webapi的UI控件的實現(xiàn)
這篇文章主要介紹了vue-autoui自匹配webapi的UI控件的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03
Vue應(yīng)用中使用xlsx庫實現(xiàn)Excel文件導出的詳細步驟
本文詳細介紹了如何在Vue應(yīng)用中使用xlsx庫來導出Excel文件,包括安裝xlsx庫、準備數(shù)據(jù)、創(chuàng)建導出方法、觸發(fā)導出操作和自定義Excel文件等步驟,xlsx庫提供了強大的API和靈活的自定義選項,使得處理Excel文件變得簡單而高效2024-10-10
詳解如何配置vue-cli3.0的vue.config.js
這篇文章主要介紹了詳解如何配置vue-cli3.0的vue.config.js,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
vue動態(tài)路由:路由參數(shù)改變,視圖不更新問題的解決
今天小編就為大家分享一篇vue動態(tài)路由:路由參數(shù)改變,視圖不更新問題的解決,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11

