這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
在使用 Vue 3 組件庫 Naive UI 的資料表格組件 DataTable 時碰到的問題,NaiveUI 的資料表格組件 DataTable 在固定頭部和列的示例中,在鍵盤操作下表格橫向滾動會有問題,本文是記錄下解決問題的程序,并最后向 Naive UI 提交 PR,
問題復現步驟:
- 滑鼠點擊表頭,此時按鍵盤左右鍵,表格橫向滾動沒問題;
- 再把滑鼠移入表體,按鍵盤左右鍵,會發現表頭滾動而表體沒動,
相關 issue:
- github.com/tusen-ai/na…
- github.com/tusen-ai/na…
- github.com/tusen-ai/na…
Naive UI 中的實作
打開 Chrome 開發者工具,可以看到固定頭和列中,表頭和表體是由兩個 table 元素單獨實作,我們遇到的問題可能就是表頭表體同步滾動的實作有點問題,具體得看原始碼中的實作驗證下,

在 DataTable 組件原始碼中涉及到滾動的相關檔案 src/data-table:
use-scroll.ts處理表格滾動事件DataTable.tsx表格組件tableParts/Header.tsx表頭組件tableParts/Body.tsx表體組件
我們按照復現步驟的操作來看看滾動的時候做了什么?
大致程序就是表頭或表體滾動時會觸發 scroll 事件,在監聽 scroll 事件的回呼中獲取 scrollLeft 值,然后設定另一部分的 scrollLeft 來同步滾動,
1. 滑鼠點擊表頭,按鍵盤左右鍵,表頭表體橫向滾動正常
當滑鼠移入表頭時,表頭監聽了 mouseenter 事件,會設當前 scrollPartRef 為 head ;按鍵盤左右鍵使表頭滾動,觸發了表頭 scroll 事件,執行 handleTableHeaderScroll 方法,該方法是由 DataTable.tsx 組件提供(provider),在 use-scroll.ts 中的 useScroll 方法匯出的;handleTableHeaderScroll 作用主要是來同步表體的滾動及一些樣式設定;代碼如下:
// use-scroll.ts
function handleTableHeaderScroll (): void {
// 判斷當前滾動的部分是不是表頭,scrollPartRef 值為 head 或 body
if (scrollPartRef.value === 'head') {
// beforeNextFrameOnce 的作用是每一幀只呼叫一次傳入的回呼
// syncScrollState 的作用是同步滾動表體和一些樣式設定
beforeNextFrameOnce(syncScrollState)
}
}
2. 再把滑鼠移入表體,按鍵盤左右鍵,表頭橫向滾動正常而表體沒動
當將滑鼠移入表體時,表體監聽了 mouseenter 事件,會設當前 scrollPartRef 為 body ,在按鍵盤左右鍵時表頭滾動,執行了表頭 scroll 事件回呼 handleTableHeaderScroll,但不滿足判斷條件 scrollPartRef.value =https://www.cnblogs.com/smileZAZ/archive/2023/07/04/=='head',沒有執行 syncScrollState 方法,
問題原因:在移入表體后,此時滑鼠焦點依舊在表頭,所以按鍵盤左右鍵時,仍然是表頭滾動及觸發 scroll 事件,執行的是 handleTableHeaderScroll 方法,而此時 scrollPartRef 的值為 body ,導致沒有執行 syncScrollState 方法來同步表體的 scrollLeft 值,最終表現表體沒有跟隨表頭滾動,
其他組件庫中的實作
在解決問題前,觀察了一下各組件庫表格組件中固定表頭和列的示例,看看是否有類似問題,查看之后發現表頭和表體都是通過兩個 table 元素來單獨實作,這就遇到一個問題,因為是兩個 table 元素,那怎么實作表頭表體同步滾動呢?以及怎么解決在 Naive UI 中遇到的問題?
Element Plus
Element Plus 中,當滑鼠點擊表頭,按鍵盤左右鍵是無法橫向滾動的,只有滑鼠焦點在表體上才能橫向滾動;也就是滾動只能由表體滾動帶動表頭滾動,
原始碼實作里面它的表格滾動條不像 Naive UI 表頭和表體都設為 overflow: scroll 來產生滾動,而是在表體包了一層封裝的滾動條組件,表頭則沒有包直接設為 overflow: hidden 不讓滾動;在滾動表體時,獲取滾動條組件的 scrollLeft 來同步表頭的 scrollLeft ,Table 組件原始碼點這里
Ant Design Vue
Ant Design Vue 的表現同 Element Plus,表頭無法滾動,只能由表體滾動帶動表頭滾動,
原始碼實作原理跟 Element Plus 差不多,它的表格表頭也是設為 oveflow: hidden 無法滾動,表體設為 overflow: auto scroll 來滾動,然后監聽表體的滾動事件 scroll獲取 scrollLeft 來同步表頭 scrollLeft ,Table 組件原始碼點這里
問題解決程序
問題復現
根據 Naive UI DataTable 原始碼中固定頭和列時同步滾動的實作方式,搞一個 demo 復現問題,
代碼實作思路:滾動分為表頭、表體兩個部分,監聽各自的滾動事件 scroll ,滾動某一個部分時,在 scroll 事件處理函式中通過設定另一部分 scrollLeft 來同步滾動,因為在設定 scrollLeft 時也會觸發 scroll 事件,這樣就會造成死回圈,所以需要判斷當前滾動的是哪個部分,這里用 scrollPartRef 變數來記錄,在滑鼠移入表頭時設 scrollPartRef 為 'head',在滑鼠移出表頭或移入表體時設 scrollPartRef 為 ‘body’,然后在滾動事件處理回呼 handleHeaderScroll / handleBodyScroll 方法中,判斷 scrollPartRef 是不是為對應的 'head' / 'body' ,是的話才會執行 syncScrollState 方法來同步另一部分的 scrollLeft ,
具體代碼如下:
Demo 在線地址:[Bug] NaiveUI-DataTable-scrolling-sync (demo) - codesandbox
/**
* Naive UI DataTable 組件滾動同步 demo 實作
*/
<template>
<div >
<p>scrollPart:{{ scrollPartRef }}</p>
<div
ref="headerRef"
@mouseenter="handleHeaderMouseenter"
@mouseleave="handleHeaderMouseleave"
@scroll="handleHeaderScroll"
>
<div tabIndex="-1">
head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head
</div>
</div>
<div
ref="bodyRef"
@mouseenter="handleBodyMouseenter"
@scroll="handleBodyScroll"
>
<div tabIndex="-1">
body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, ref } from "vue";
const scrollPartRef = ref(); // 當前滾動部分,值為 'head' 或 'body'
const headerRef = ref();
const bodyRef = ref();
function handleHeaderScroll() {
if (scrollPartRef.value === "head") {
syncScrollState();
}
}
function handleBodyScroll() {
if (scrollPartRef.value === "body") {
syncScrollState();
}
}
// 同步滾動
let scrollLeft = 0;
function syncScrollState() {
if (scrollPartRef.value === "head") {
scrollLeft = headerRef.value.scrollLeft;
bodyRef.value.scrollLeft = scrollLeft;
} else {
scrollLeft = bodyRef.value.scrollLeft;
headerRef.value.scrollLeft = scrollLeft;
}
}
function handleHeaderMouseenter() {
scrollPartRef.value = "head";
}
function handleHeaderMouseleave() {
scrollPartRef.value = "body";
}
function handleBodyMouseenter() {
scrollPartRef.value = "body";
}
</script>
<style>
.wrap {
width: 600px;
}
.content {
width: 1000px;
height: 80px;
background-color: lightblue;
}
.header {
padding: 10px;
background-color: lightgray;
overflow: auto;
margin-bottom: 20px;
}
.body {
padding: 10px;
background-color: lightgray;
overflow: auto;
}
.content {
border: 1px solid orange;
}
</style>
問題分析
想下使瀏覽器原生滾動條滾動的互動操作有幾種:
- 觸控板手勢滾動
- 滑鼠按住滾動條拖動
- 鍵盤 shift 鍵 + 滑鼠滾輪滾動
- 滑鼠焦點在滾動內容上,接著用鍵盤左右鍵滾動
- 還有么?
概括下來就是分為三種:觸控板、滑鼠、鍵盤,作者實作的時候可能沒考慮到鍵盤操作的場景,
問題原因上面分析過,在滑鼠點擊表頭后移入表體,scrollPartRef 會被設為 'body' ,而此時滑鼠焦點依舊在表頭上,當操作鍵盤方向鍵,滾動的是表頭,執行它的 handleHeaderScroll 方法,但不滿足判斷條件 scrollPartRef.value =https://www.cnblogs.com/smileZAZ/archive/2023/07/04/=='head',沒有執行 syncScrollState 方法,導致表體沒有同步滾動,
現在能想到的解決方案有兩種:一種就是參考其他組件庫中的方案,不讓表頭能主動滾動,只能由表體滾動帶動表頭滾動;一種就是修復復現操作下的問題,
我認為現在的實作方式太復雜了,需要監聽表頭、表體的滑鼠事件 mouseenter 、mouseleave來預設當前滾動部分 scrollPartRef ,如果按照這種思路,要修復鍵盤操作下的問題,是不是還要監聽當前焦點focus 事件然后做判斷,有沒有更簡單的方式?
解決思路
我的想法是只監聽 scroll 事件能不能做到同步滾動,現在有表頭、表體兩個滾動部分,那么可分為主動滾動和被動滾動;在未滾動前,我們不預設主動滾動是哪部分(即不設定 scrollPartRef),等到真正滾動的時候,如果我們能知道主動滾動的是哪部分,這樣就能獲取主動滾動部分的 scrollLeft ,去設定被動滾動部分的 scrollLeft ,以此實作同步滾動,如果大家有更好的解決思路,歡迎討論!
怎么判斷主動滾動的是哪部分?
當時給 Naive UI 提 PR 的時候想到的是第一種思路,但是我覺得第二種思路更好一點,后續重新提交一個,
第一種思路:在每次滾動中取表頭或表體的 scrollLeft 和上一次滾動記錄下的 lastScrollLeft (初始為 0)比較來判斷當前主動滾動部分是哪個,這里取表頭部分的 scrollLeft,如果差值不為 0,說明當前主動滾動部分為表頭(即 scrollPartRef = ‘head’),否則為表體,
const scrollPartRef = ref(); // 當前主動滾動部分
const headerRef = ref();
const bodyRef = ref();
function handleHeaderScroll() {
if (scrollPartRef.value !== "body") {
syncScrollState();
} else {
// 每次滾動結束,置空
scrollPartRef.value = undefined;
}
}
function handleBodyScroll() {
if (scrollPartRef.value !== "head") {
syncScrollState();
} else {
// 每次滾動結束,置空
scrollPartRef.value = undefined;
}
}
let lastScrollLeft = 0;
function syncScrollState() {
if (!scrollPart.value) {
// 取 header 的 scrollLeft 跟上一次滾動記錄的 scrollLeft 比較
const directionHead = lastScrollLeft - headerRef.value.scrollLeft;
// 不為 0 說明 header 滾動了,主動滾動即為 head,否則為 body
scrollPart.value = directionHead !== 0 ? "head" : "body";
}
if (scrollPart.value === "head") {
lastScrollLeft = headerRef.value.scrollLeft;
bodyRef.value.scrollLeft = lastScrollLeft;
} else {
lastScrollLeft = bodyRef.value.scrollLeft;
headerRef.value.scrollLeft = lastScrollLeft;
}
}
第二種思路:主動滾動部分肯定會先觸發滾動事件,所以可以在表頭或表體的 scroll 事件處理函式中判斷 scrollPartRef 是否存在,不存在則將 scrollPartRef 設為對應的 'head' \ 'body'(即為主動滾動部分),然后呼叫syncScrollState同步被動滾動部分的scrollLeft,代碼如下:
function handleHeaderScroll() {
if(!scrollPart.value) {
scrollPartRef.value = 'head'
}
if (scrollPartRef.value === "head") {
syncScrollState();
} else {
// 每次滾動結束,置空
scrollPartRef.value = undefined
}
}
function handleBodyScroll() {
if(!scrollPart.value) {
scrollPartRef.value = 'body'
}
if (scrollPartRef.value === "body") {
syncScrollState();
} else {
// 每次滾動結束,置空
scrollPartRef.value = undefined
}
}
function syncScrollState() {
if (scrollPart.value === "head") {
lastScrollLeft = headerRef.value.scrollLeft;
bodyRef.value.scrollLeft = lastScrollLeft;
} else {
lastScrollLeft = bodyRef.value.scrollLeft;
headerRef.value.scrollLeft = lastScrollLeft;
}
怎么判斷滾動結束?
我們需要在每一次滾動結束后置空 scrollPartRef,否則同步會出問題,被動滾動部分在被設定 scrollLeft 時也會觸發 scroll 事件,而被動滾動部分的 scroll 事件會晚于主動滾動的 scroll 事件觸發,所以可以認為被動滾動事件執行完滾動就結束了,在它的事件回呼處理中的 else 分支置空 scrollPartRef ,如上代碼所示,
但是這樣判斷結束在 Safari 中還是有點有問題的,詳見下面遺留問題,
完整代碼
Demo 在線地址:[Fix] NaiveUI-DataTable-scrolling-sync (demo) - codesandbox
<template>
<div >
<p>scrollPart:{{ scrollPart }}</p>
<div ref="headerRef" @scroll="handleHeaderScroll">
<div tabIndex="-1">
head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head-head
</div>
</div>
<div ref="bodyRef" @scroll="handleBodyScroll">
<div tabIndex="-1">
body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body-body
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const scrollPart = ref() // 當前主動滾動部分
const headerRef = ref()
function handleHeaderScroll() {
if (scrollPart.value !== 'body') {
console.log('<<< head scroll start >>>')
syncScrollState()
} else {
scrollPart.value = undefined
console.log('<<< body scroll end >>>')
console.log('\n')
}
}
const bodyRef = ref()
function handleBodyScroll() {
if (scrollPart.value !== 'head') {
console.log('<<< body scroll start >>>')
syncScrollState()
} else {
scrollPart.value = undefined
console.log('<<< head scroll end >>>')
console.log('\n')
}
}
let lastScrollLeft = 0
function syncScrollState() {
// 取 header 的 scrollLeft 跟上一次滾動記錄的 scrollLeft 比較
const directionHead = lastScrollLeft - headerRef.value.scrollLeft
// 不為 0 說明 header 滾動了,主動滾動即為 head,否則為 body
scrollPart.value = directionHead !== 0 ? 'head' : 'body'
if (scrollPart.value === 'head') {
lastScrollLeft = headerRef.value.scrollLeft
bodyRef.value.scrollLeft = lastScrollLeft
} else {
lastScrollLeft = bodyRef.value.scrollLeft
headerRef.value.scrollLeft = lastScrollLeft
}
console.log('final scrollLeft', lastScrollLeft)
}
</script>
<style>
.wrap {
width: 600px;
}
.content {
width: 1000px;
height: 80px;
background-color: lightblue;
}
.header {
padding: 10px;
background-color: lightgray;
overflow: auto;
margin-bottom: 20px;
}
.body {
padding: 10px;
background-color: lightgray;
overflow: auto;
}
.content {
border: 1px solid orange;
}
</style>
PR
github.com/tusen-ai/na…
遺留問題
雖然解決了鍵盤操作的問題,但是后面發現在 Safari 瀏覽器中使用觸控板快速滑動會有點小問題,這是由于在 Safari 中,滾動會有彈性效果導致的,復現步驟:
- 滑鼠先點擊表頭,讓焦點在表頭上;
- 滑鼠移入表體,使用觸控板快速滑動,有彈性效果;
- 按鍵盤左鍵,第一下表頭滾動,表體沒動,
原因是因為快速滑動表體(主動滾動部分)時,表頭(被動滾動部分)到達邊界后就不會再觸發 scroll 事件了,而表體因為彈性效果依舊在觸發 scroll 事件,導致 scrollPartRef 一直為 'body',未被清空,后面再按鍵盤滾動表頭時,事件處理中條件 scrollPartRef.value =https://www.cnblogs.com/smileZAZ/archive/2023/07/04/=='head' 不滿足,未執行 syncScrollState 方法,導致了表體未同步滾動,根本原因就是我們沒法正確判斷滾動什么時候結束,如果能知道什么時候滾動結束,那么在滾動結束時重置 scrollPartRef 就不會有問題了,
聽說現在有了 scrollend ,但是兼容性不行,
本文轉載于:
https://juejin.cn/post/7251786381483376695
如果對您有所幫助,歡迎您點個關注,我會定時更新技術檔案,大家一起討論學習,一起進步,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556612.html
標籤:其他
下一篇:返回列表

