记录--组件库的 Table 组件表头表体是如何实现同步滚动?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
在使用 Vue 3 组件库 Naive UI 的数据表格组件 DataTable 时碰到的问题,NaiveUI 的数据表格组件 DataTable 在固定头部和列的示例中,在键盘操作下表格横向滚动会有问题,本文是记录下解决问题的过程,并最后向 Naive UI 提交 PR。
问题复现步骤:
- 鼠标点击表头,此时按键盘左右键,表格横向滚动没问题;
- 再把鼠标移入表体,按键盘左右键,会发现表头滚动而表体没动。
相关 issue:
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 === '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 class="wrap">
<p>scrollPart:{{ scrollPartRef }}</p>
<div
ref="headerRef"
class="header"
@mouseenter="handleHeaderMouseenter"
@mouseleave="handleHeaderMouseleave"
@scroll="handleHeaderScroll"
>
<div class="content" 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"
class="body"
@mouseenter="handleBodyMouseenter"
@scroll="handleBodyScroll"
>
<div class="content" 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 === '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 class="wrap">
<p>scrollPart:{{ scrollPart }}</p>
<div ref="headerRef" class="header" @scroll="handleHeaderScroll">
<div class="content" 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" class="body" @scroll="handleBodyScroll">
<div class="content" 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
遗留问题
虽然解决了键盘操作的问题,但是后面发现在 Safari 浏览器中使用触控板快速滑动会有点小问题,这是由于在 Safari 中,滚动会有弹性效果导致的,复现步骤:
- 鼠标先点击表头,让焦点在表头上;
- 鼠标移入表体,使用触控板快速滑动,有弹性效果;
- 按键盘左键,第一下表头滚动,表体没动。
原因是因为快速滑动表体(主动滚动部分)时,表头(被动滚动部分)到达边界后就不会再触发 scroll
事件了,而表体因为弹性效果依旧在触发 scroll
事件,导致 scrollPartRef
一直为 'body'
,未被清空,后面再按键盘滚动表头时,事件处理中条件 scrollPartRef.value === 'head'
不满足,未执行 syncScrollState
方法,导致了表体未同步滚动。根本原因就是我们没法正确判断滚动什么时候结束,如果能知道什么时候滚动结束,那么在滚动结束时重置 scrollPartRef
就不会有问题了。
听说现在有了 scrollend
,但是兼容性不行。
本文转载于:
https://juejin.cn/post/7251786381483376695
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
记录--组件库的 Table 组件表头表体是如何实现同步滚动?的更多相关文章
- ElementUI2.0组件库el-table表格组件如何自定义表头?
效果图: npm run dev 编译项目之后,报错,要使用jsx语法需要先安装编译插件 1.安装下列安装包 npm install babel-plugin-syntax-jsx --save-de ...
- Vue3 企业级优雅实战 - 组件库框架 - 5 组件库通用工具包
该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...
- Vue3 企业级优雅实战 - 组件库框架 - 7 组件库文档的开发和构建
该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...
- ie下div模拟的表格,表头表体无法对齐
现象:ie下,如果某个区域滚动显示表格内容(div模拟的table),表体出现滚动条的时候,会跟表头无法对齐. 解决方法:1.首先需要知道两个高度:表体最大高度height1.目前表体要显示的内容高度 ...
- Bootstrap Blazor 组件库 Row 布局组件(栅格系统)
原文链接:https://www.cnblogs.com/ysmc/p/16133351.html 在 Bootstrap 中,栅格相信大家都很熟悉,简直就是布局神器啊,Bootstrap Blazo ...
- 从0搭建vue3组件库:Shake抖动组件
先看下效果 其实就是个抖动效果组件,实现起来也非常简单.之所以做这样一个组件是为了后面写Form表单的时候会用到它做一个规则校验,比如下面一个简单的登录页面,当点击登录会提示用户哪个信息没输入,当然这 ...
- Blazor Bootstrap 组件库浏览器通知组件介绍
通知组件 通过浏览器API发送通知信息 , 桌面浏览器表现为右下角系统提示框弹出消息, 移动浏览器表现为弹窗或者到消息列表, blazor页面不在前台也可以通过本组件提醒用户. DEMO https: ...
- Vue3 企业级优雅实战 - 组件库框架 - 4 组件库的 CSS 架构
在前一篇文章中分享了搭建组件库的基本开发环境.创建了 foo 组件模块和组件库入口模块,本文分享组件库的样式架构设计. 1 常见的 CSS 架构模式 常见的 CSS 架构模式有很多:OOCSS.ACS ...
- Vue3 企业级优雅实战 - 组件库框架 - 11 组件库的打包构建和发布
回顾第一篇文章中谈到的组件库的几个方面,只剩下最后的.也是最重要的组件库的打包构建.本地发布.远程发布了. 1 组件库构建 组件库的入口是 packages/yyg-demo-ui,构建组件库有两个步 ...
- 对Big Table进行全表更新,导致 Replication 同步数据的过程十分缓慢
在Publisher database中更新一个big table,数据行数是3.4亿多.由于没有更新 clustered Index key,因此,只产生了3.4亿多个Update Commands ...
随机推荐
- NebulaGraph入门介绍
NebulaGraph入门介绍 什么是图数据库? 图数据库就会是存储图形网络并能从中检索信息的数据库. 图数据库在处理关联关系上有极大的优势,它以图论为理论基础,使用图模型,将关联数据的实体作为顶点( ...
- NC21125 践踏
题目链接 题目 题目描述 首先给定一个定值k,支持如下操作(在数轴上) 加入一条线段[l,r] 删除一条已经存在的线段 给定x,问有多少个区间包含x+kt,其中t是一个整数变量,即t ∈ Z 比如说当 ...
- NVME(学习杂谈)—Asynchronous Event
Asynchronous Event Request Host Software Recommendations 当一个异步事件请求完成(提供Event Type,Event Information, ...
- django学习第十二天--ajax请求和csrftoken认证的三种方式
基于cookie的登录认证装饰器 def check_login(f): def inner(request,*args,**kwargs): is_login = request.COOKIES.g ...
- Python函数每日一讲 - 简洁快速学会globals()函数
引言 在 Python 中,globals() 函数是一个强大的工具,它允许您访问全局命名空间中的所有变量和函数.本文将深入探讨 globals() 函数的语法.用法以及实际应用场景,帮助大家更好地理 ...
- 谈一谈如何使用etcd中的事务
本文内容来源于自己学习时所做的记录,主要来源于文章最后的参考链接,如有侵权,请联系删除,谢谢! etcd 是一个 key/value 类型的数据库.既然我们需要存储数据,必然会面临这样一个需求,即希望 ...
- 为什么带NOLOCK的查询语句还会造成阻塞
背景 客户反映HIS数据库在11点出现了长时间的阻塞,直到手动KILL掉阻塞的源头.请我们协助分析原因,最终定位到.NET程序中使用的SqlDataReader未正常关闭导致. 现象 登录SQL专家云 ...
- 使用 maven 的 `wagon-maven-plugin`插件 快速部署 到不同的 环境
profile 在pom文件中配置 开发和测试环境的 profile信息, <profiles> <profile> <!-- 开发环境 --> <id> ...
- 俄罗斯套娃 (Matryoshka) 嵌入模型概述
在这篇博客中,我们将向你介绍俄罗斯套娃嵌入的概念,并解释为什么它们很有用.我们将讨论这些模型在理论上是如何训练的,以及你如何使用 Sentence Transformers 来训练它们. 除此之外,我 ...
- Docker下搭建MySql主从复制
在Docker环境下搭建MySql主从复制,阅读此文章默认读者具备基础的Docker命令操作. 一.环境 1.Docker版本:Docker version 24.0.5, build ced0996 ...