最新要闻

广告

手机

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

家电

记录--组件库的 Table 组件表头表体是如何实现同步滚动?|每日时讯

来源:博客园

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

前言

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


(资料图)

问题复现步骤:

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

相关 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事件,会设当前 scrollPartRefhead;按键盘左右键使表头滚动,触发了表头 scroll事件,执行 handleTableHeaderScroll方法,该方法是由 DataTable.tsx组件提供(provider),在 use-scroll.ts中的 useScroll方法导出的;handleTableHeaderScroll作用主要是来同步表体的滚动及一些样式设置;代码如下:

//use-scroll.tsfunctionhandleTableHeaderScroll():void{//判断当前滚动的部分是不是表头,scrollPartRef值为head或bodyif(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

/***NaiveUIDataTable组件滚动同步demo实现*/<scriptsetuplang="ts">import{defineComponent,ref}from"vue";constscrollPartRef=ref();//当前滚动部分,值为"head"或"body"constheaderRef=ref();constbodyRef=ref();functionhandleHeaderScroll(){if(scrollPartRef.value==="head"){syncScrollState();}}functionhandleBodyScroll(){if(scrollPartRef.value==="body"){syncScrollState();}}//同步滚动letscrollLeft=0;functionsyncScrollState(){if(scrollPartRef.value==="head"){scrollLeft=headerRef.value.scrollLeft;bodyRef.value.scrollLeft=scrollLeft;}else{scrollLeft=bodyRef.value.scrollLeft;headerRef.value.scrollLeft=scrollLeft;}}functionhandleHeaderMouseenter(){scrollPartRef.value="head";}functionhandleHeaderMouseleave(){scrollPartRef.value="body";}functionhandleBodyMouseenter(){scrollPartRef.value="body";}</script>

问题分析

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

  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’),否则为表体。

constscrollPartRef=ref();//当前主动滚动部分constheaderRef=ref();constbodyRef=ref();functionhandleHeaderScroll(){if(scrollPartRef.value!=="body"){syncScrollState();}else{//每次滚动结束,置空scrollPartRef.value=undefined;}}functionhandleBodyScroll(){if(scrollPartRef.value!=="head"){syncScrollState();}else{//每次滚动结束,置空scrollPartRef.value=undefined;}}letlastScrollLeft=0;functionsyncScrollState(){if(!scrollPart.value){//取header的scrollLeft跟上一次滚动记录的scrollLeft比较constdirectionHead=lastScrollLeft-headerRef.value.scrollLeft;//不为0说明header滚动了,主动滚动即为head,否则为bodyscrollPart.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。代码如下:
functionhandleHeaderScroll(){if(!scrollPart.value){scrollPartRef.value="head"}if(scrollPartRef.value==="head"){syncScrollState();}else{//每次滚动结束,置空scrollPartRef.value=undefined}}functionhandleBodyScroll(){if(!scrollPart.value){scrollPartRef.value="body"}if(scrollPartRef.value==="body"){syncScrollState();}else{//每次滚动结束,置空scrollPartRef.value=undefined}}functionsyncScrollState(){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

<scriptsetuplang="ts">import{ref}from"vue"constscrollPart=ref()//当前主动滚动部分constheaderRef=ref()functionhandleHeaderScroll(){if(scrollPart.value!=="body"){console.log("<<>>")syncScrollState()}else{scrollPart.value=undefinedconsole.log("<<>>")console.log("\n")}}constbodyRef=ref()functionhandleBodyScroll(){if(scrollPart.value!=="head"){console.log("<<>>")syncScrollState()}else{scrollPart.value=undefinedconsole.log("<<>>")console.log("\n")}}letlastScrollLeft=0functionsyncScrollState(){//取header的scrollLeft跟上一次滚动记录的scrollLeft比较constdirectionHead=lastScrollLeft-headerRef.value.scrollLeft//不为0说明header滚动了,主动滚动即为head,否则为bodyscrollPart.value=directionHead!==0?"head":"body"if(scrollPart.value==="head"){lastScrollLeft=headerRef.value.scrollLeftbodyRef.value.scrollLeft=lastScrollLeft}else{lastScrollLeft=bodyRef.value.scrollLeftheaderRef.value.scrollLeft=lastScrollLeft}console.log("finalscrollLeft",lastScrollLeft)}</script>

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

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

关键词: