这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

在使用 Vue 3 组件库 Naive UI 的数据表格组件 DataTable 时碰到的问题,NaiveUI 的数据表格组件 DataTable 在固定头部和列的示例中,在键盘操作下表格横向滚动会有问题,本文是记录下解决问题的过程,并最后向 Naive UI 提交 PR。

问题复现步骤:

  1. 鼠标点击表头,此时按键盘左右键,表格横向滚动没问题;
  2. 再把鼠标移入表体,按键盘左右键,会发现表头滚动而表体没动。

相关 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 事件,会设当前 scrollPartRefhead ;按键盘左右键使表头滚动,触发了表头 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 事件,会设当前 scrollPartRefbody ,在按键盘左右键时表头滚动,执行了表头 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>

问题分析

想下使浏览器原生滚动条滚动的交互操作有几种:

  1. 触控板手势滚动
  2. 鼠标按住滚动条拖动
  3. 键盘 shift 键 + 鼠标滚轮滚动
  4. 鼠标焦点在滚动内容上,接着用键盘左右键滚动
  5. 还有么?

概括下来就是分为三种:触控板、鼠标、键盘,作者实现的时候可能没考虑到键盘操作的场景。

问题原因上面分析过,在鼠标点击表头后移入表体,scrollPartRef 会被设为 'body' ,而此时鼠标焦点依旧在表头上,当操作键盘方向键,滚动的是表头,执行它的 handleHeaderScroll 方法,但不满足判断条件 scrollPartRef.value === 'head',没有执行 syncScrollState 方法,导致表体没有同步滚动。

现在能想到的解决方案有两种:一种就是参考其他组件库中的方案,不让表头能主动滚动,只能由表体滚动带动表头滚动;一种就是修复复现操作下的问题。

我认为现在的实现方式太复杂了,需要监听表头、表体的鼠标事件 mouseentermouseleave来预设当前滚动部分 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

github.com/tusen-ai/na…

遗留问题

虽然解决了键盘操作的问题,但是后面发现在 Safari 浏览器中使用触控板快速滑动会有点小问题,这是由于在 Safari 中,滚动会有弹性效果导致的,复现步骤:

  1. 鼠标先点击表头,让焦点在表头上;
  2. 鼠标移入表体,使用触控板快速滑动,有弹性效果;
  3. 按键盘左键,第一下表头滚动,表体没动。

原因是因为快速滑动表体(主动滚动部分)时,表头(被动滚动部分)到达边界后就不会再触发 scroll 事件了,而表体因为弹性效果依旧在触发 scroll 事件,导致 scrollPartRef 一直为 'body',未被清空,后面再按键盘滚动表头时,事件处理中条件 scrollPartRef.value === 'head' 不满足,未执行 syncScrollState 方法,导致了表体未同步滚动。根本原因就是我们没法正确判断滚动什么时候结束,如果能知道什么时候滚动结束,那么在滚动结束时重置 scrollPartRef 就不会有问题了。

听说现在有了 scrollend ,但是兼容性不行。

本文转载于:

https://juejin.cn/post/7251786381483376695

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--组件库的 Table 组件表头表体是如何实现同步滚动?的更多相关文章

  1. ElementUI2.0组件库el-table表格组件如何自定义表头?

    效果图: npm run dev 编译项目之后,报错,要使用jsx语法需要先安装编译插件 1.安装下列安装包 npm install babel-plugin-syntax-jsx --save-de ...

  2. Vue3 企业级优雅实战 - 组件库框架 - 5 组件库通用工具包

    该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...

  3. Vue3 企业级优雅实战 - 组件库框架 - 7 组件库文档的开发和构建

    该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...

  4. ie下div模拟的表格,表头表体无法对齐

    现象:ie下,如果某个区域滚动显示表格内容(div模拟的table),表体出现滚动条的时候,会跟表头无法对齐. 解决方法:1.首先需要知道两个高度:表体最大高度height1.目前表体要显示的内容高度 ...

  5. Bootstrap Blazor 组件库 Row 布局组件(栅格系统)

    原文链接:https://www.cnblogs.com/ysmc/p/16133351.html 在 Bootstrap 中,栅格相信大家都很熟悉,简直就是布局神器啊,Bootstrap Blazo ...

  6. 从0搭建vue3组件库:Shake抖动组件

    先看下效果 其实就是个抖动效果组件,实现起来也非常简单.之所以做这样一个组件是为了后面写Form表单的时候会用到它做一个规则校验,比如下面一个简单的登录页面,当点击登录会提示用户哪个信息没输入,当然这 ...

  7. Blazor Bootstrap 组件库浏览器通知组件介绍

    通知组件 通过浏览器API发送通知信息 , 桌面浏览器表现为右下角系统提示框弹出消息, 移动浏览器表现为弹窗或者到消息列表, blazor页面不在前台也可以通过本组件提醒用户. DEMO https: ...

  8. Vue3 企业级优雅实战 - 组件库框架 - 4 组件库的 CSS 架构

    在前一篇文章中分享了搭建组件库的基本开发环境.创建了 foo 组件模块和组件库入口模块,本文分享组件库的样式架构设计. 1 常见的 CSS 架构模式 常见的 CSS 架构模式有很多:OOCSS.ACS ...

  9. Vue3 企业级优雅实战 - 组件库框架 - 11 组件库的打包构建和发布

    回顾第一篇文章中谈到的组件库的几个方面,只剩下最后的.也是最重要的组件库的打包构建.本地发布.远程发布了. 1 组件库构建 组件库的入口是 packages/yyg-demo-ui,构建组件库有两个步 ...

  10. 对Big Table进行全表更新,导致 Replication 同步数据的过程十分缓慢

    在Publisher database中更新一个big table,数据行数是3.4亿多.由于没有更新 clustered Index key,因此,只产生了3.4亿多个Update Commands ...

随机推荐

  1. NebulaGraph入门介绍

    NebulaGraph入门介绍 什么是图数据库? 图数据库就会是存储图形网络并能从中检索信息的数据库. 图数据库在处理关联关系上有极大的优势,它以图论为理论基础,使用图模型,将关联数据的实体作为顶点( ...

  2. NC21125 践踏

    题目链接 题目 题目描述 首先给定一个定值k,支持如下操作(在数轴上) 加入一条线段[l,r] 删除一条已经存在的线段 给定x,问有多少个区间包含x+kt,其中t是一个整数变量,即t ∈ Z 比如说当 ...

  3. NVME(学习杂谈)—Asynchronous Event

    Asynchronous Event Request Host Software Recommendations 当一个异步事件请求完成(提供Event Type,Event Information, ...

  4. django学习第十二天--ajax请求和csrftoken认证的三种方式

    基于cookie的登录认证装饰器 def check_login(f): def inner(request,*args,**kwargs): is_login = request.COOKIES.g ...

  5. Python函数每日一讲 - 简洁快速学会globals()函数

    引言 在 Python 中,globals() 函数是一个强大的工具,它允许您访问全局命名空间中的所有变量和函数.本文将深入探讨 globals() 函数的语法.用法以及实际应用场景,帮助大家更好地理 ...

  6. 谈一谈如何使用etcd中的事务

    本文内容来源于自己学习时所做的记录,主要来源于文章最后的参考链接,如有侵权,请联系删除,谢谢! etcd 是一个 key/value 类型的数据库.既然我们需要存储数据,必然会面临这样一个需求,即希望 ...

  7. 为什么带NOLOCK的查询语句还会造成阻塞

    背景 客户反映HIS数据库在11点出现了长时间的阻塞,直到手动KILL掉阻塞的源头.请我们协助分析原因,最终定位到.NET程序中使用的SqlDataReader未正常关闭导致. 现象 登录SQL专家云 ...

  8. 使用 maven 的 `wagon-maven-plugin`插件 快速部署 到不同的 环境

    profile 在pom文件中配置 开发和测试环境的 profile信息, <profiles> <profile> <!-- 开发环境 --> <id> ...

  9. 俄罗斯套娃 (Matryoshka) 嵌入模型概述

    在这篇博客中,我们将向你介绍俄罗斯套娃嵌入的概念,并解释为什么它们很有用.我们将讨论这些模型在理论上是如何训练的,以及你如何使用 Sentence Transformers 来训练它们. 除此之外,我 ...

  10. Docker下搭建MySql主从复制

    在Docker环境下搭建MySql主从复制,阅读此文章默认读者具备基础的Docker命令操作. 一.环境 1.Docker版本:Docker version 24.0.5, build ced0996 ...