百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

哇噻,简直是个天才,无需scroll事件就能监听到元素滚动

yuyutoo 2024-10-12 00:22 6 浏览 0 评论

哇噻,简直是个天才,无需scroll事件就能监听到元素滚动

1. 前言

最近在做 toolTip 弹窗相关组件封装,实现的效果就是可以通过hover或点击在元素的上面或者下面能够出现一个弹框,类似下面这样

这时我遇到一个问题,因为我想当这个弹窗快要滚出屏幕之外时能够从由上面弹出变到由下面弹出,本来想着直接监听 scroll 事件就能搞定的,但是仔细一想 scroll 事件到底要绑定到那个 DOM 上呢? 因为很多时候滚动条出现的元素并不是最外层的 body 或者 html 可能是任意一个元素上的滚动条。这个时候就无法通过绑定 scroll 事件来监听元素滚动了。

2. 问题分析

我脑海中首先 IntersectionObserver 这个 API,但是这个 API 只能用来 监测目标元素与视窗(viewport)的交叉状态,也就是当我的元素滚出或者滚入的时候可以触发该监听的回调。

js复制代码new IntersectionObserver((event) => {
        refresh();
      }, {
       // threshold 用来表示元素在视窗中显示的交叉比例显示
       // 设置的是 0 即表示元素完全移出视窗,1 或者完全进入视窗时触发回调
       // 0表示元素本身在视口中的占比0%, 1表示元素本身在视口中的占比为100%
       // 0.1表示元素本身在视口中的占比1%,0.9表示元素本身在视口中的占比为90%
        threshold: [0, 1, 0.1, 0.9]
      });

这样就可以在元素快要移出屏幕,或者移入屏幕时触发回调了,但是这样会有一个问题

当弹窗移出屏幕时,可以很轻松的监听到,并把弹窗移动到下方,但是当弹窗滚入的时候就有问题了

可以看到完全进入之后,这个时候由于顶部空间不够,还需要继续往下滚才能将弹窗由底部移动到顶部。但是已经无法再触发 IntersectionObserver 和视口交叉的回调事件了,因为元素已经完全在视窗内了。 也就是说用这种方案,元素一旦滚出去之后,再回来的时候就无法复原了。

3. 把问题抛给别人

既然自己很难解决,那就看看别人是怎么解决这个问题的吧,我直接上 饿了么UI 上看看它的弹窗组件是怎么做的,于是我找到了 floating-ui 也就是原来的 popper.js 现在改名字了。

在文档中,我找到自动更新这块,也就是 floating-ui 通过监听器来实现自动更新弹窗位置。 到这里就可以看看 floating-ui 的源码了。

js
复制代码import {autoUpdate} from '@floating-ui/dom';

可以看到这个方法是放在 'floating-ui/dom'下面的

源码地址:github.com/floating-ui…

进入 floating-ui 的 github 地址,找到 packagesdom 下的 src 目录下,就可以看到想要的 autoUpdate.ts 自动更新的具体实现了。

4. 天才的想法

抛去其它不重要的东西,实现自动更新主要就是其中的 refresh 方法,先看一下代码

js复制代码function refresh(skip = false, threshold = 1) {
    // 清理操作,清理上一次定时器和监听
    cleanup();

    // 获取元素的位置和尺寸信息
    const {
        left,
        top,
        width,
        height
    } = element.getBoundingClientRect();
        
        if (!skip) {
          // 这里更新弹窗的位置
          onMove();
        }
        
    // 如果元素的宽度或高度不存在,则直接返回
    if (!width || !height) {
        return;
    }

    // 计算元素相对于视口四个方向的偏移量
    const insetTop = Math.floor(top);
    const insetRight = Math.floor(root.clientWidth - (left + width));
    const insetBottom = Math.floor(root.clientHeight - (top + height));
    const insetLeft = Math.floor(left);
  // 这里就是元素的位置
    const rootMargin = `${-insetTop}px ${-insetRight}px ${-insetBottom}px ${-insetLeft}px`;

    // 定义 IntersectionObserver 的选项
    const options = {
        rootMargin,
        threshold: Math.max(0, Math.min(1, threshold)) || 1,
    };

    let isFirstUpdate = true;

    // 处理 IntersectionObserver 的观察结果
    function handleObserve(entries) {
                // 这里事件会把元素和视口交叉的比例返回
        const ratio = entries[0].intersectionRatio;
        // 判断新的视口比例和老的是否一致,如果一致说明没有变化
        if (ratio !== threshold) {
            if (!isFirstUpdate) {
                return refresh();
            }

            if (!ratio) {
                    // 即元素完全不可见时,也就是ratio = 0时,代码设置了一个定时器。
                    // 这个定时器的作用是在短暂的延迟(100毫秒)后,再次调用 `refresh` 函数,
                    // 这次传递一个非常小的阈值 `1e-7`。这样可以在元素完全不可见时,保证重新触发监听
                timeoutId = setTimeout(() => {
                    refresh(false, 1e-7);
                }, 100);
            } else {
                refresh(false, ratio);
            }
        }

        isFirstUpdate = false;
    }

        // 创建 IntersectionObserver 对象并开始观察元素
        io = new IntersectionObserver(handleObserve, options);
        // 监听元素
        io.observe(element);
}

refresh(true);

可以发现代码其实不复杂,主要实现还是依赖于IntersectionObserver,但是其中最重要的有几个点,我详细介绍一下

4.1 rootMargin

最重要的其实就是 rootMargin, rootMargin到底是做啥用的呢?

我上面说了 IntersectionObserver监测目标元素与视窗(viewport)的交叉状态,而这个 rootMargin 就是可以将这个视窗缩小。

比如我设置 rootMargin 为 "-50px -30px -20px -30px",注意这里 rootMarginmargin 类似,都是按照 上 右 下 左 来设置的

可以看到这样,当元素距离顶部 50px 就触发了事件。而不必等到元素完全滚动到视口。

既然这样,当我设置 rootMargin 就是该元素本身的位置,不就可以实现只要元素一滚动,元素就与视口发生了交叉,触发事件了吗?

4.2 循环监听事件

仅仅将视口缩小到该元素本身的位置还是不够,因为只要一滚动,元素的位置就发生了改变,即视口的位置也需要跟随着元素的位置变化进行变化

js复制代码if (ratio !== threshold) {
        if (!isFirstUpdate) {
           return refresh();
        }
        if (!ratio) {
            // 即元素完全不可见时,也就是ratio = 0时,代码设置了一个定时器。
            // 这个定时器的作用是在短暂的延迟(100毫秒)后,再次调用 `refresh` 函数,
            // 这次传递一个非常小的阈值 `1e-7`。这样可以在元素在视口不可见时,保证可以重新触发监听
            timeoutId = setTimeout(() => {
                    refresh(false, 1e-7);
            }, 100);
        } else {
                refresh(false, ratio);
        }
}

也就是这里,可以看到每一次元素视口交叉的比例变化后,都重新调用了 refresh 方法,根据当前元素和屏幕的新的距离,创建一个新的监听器。

这样的话也就实现了类似 scroll 的效果,通过不断变化的视口来确认元素的位置是否发生了变化

5. 结语

所以说有时候思路还是没有打开,刚看到这个实现思路确实惊到我了,没有想到借助 rootMargin 可以实现类似 scroll 监听的效果。很多时候得多看看别人的实现思路,学习学习大牛写的代码和实现方式,对自己实现类似的效果相当有帮助

floating-ui

作者:码头的薯条

链接:https://juejin.cn/post/7344164779630673946

相关推荐

网站建设:从新手到高手

现代化网站应用领域非常广泛,从个人形象网站展示、企业商业网站运作、到政府公益等服务网站,各行各业都需要网站建设。大体上可以归结四类:宣传型网站设计、产品型网站制作、电子商务型网站建设、定制型功能网站开...

JetBrains 推出全新 AI 编程工具 Junie,助力高效开发

JetBrains宣布推出名为Junie的全新AI编程工具。这款工具不仅能执行简单的代码生成与检查任务,还能应对编写测试、验证结果等复杂项目,为开发者提供全方位支持。根据SWEBench...

AI也能写代码!代码生成、代码补全、注释生成、代码翻译轻松搞定

清华GLM技术团队打造的多语言代码生成模型CodeGeeX近期更新了新的开源版本「CodeGeeX2-6B」。CodeGeeX2是多语言代码生成模型CodeGeeX的第二代模型,不同于一代CodeG...

一键生成前后端代码,一个36k星的企业级低代码平台

「企业级低代码平台」前后端分离架构SpringBoot2.x,SpringCloud,AntDesign&Vue,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任...

Gitee 代码托管实战指南:5 步完成本地项目云端同步(附避坑要点)

核心流程拆解:远程仓库的搭建登录Gitee官网(注册账号比较简单,大家自行操作),点击“新建仓库”,建议勾选“初始化仓库”和“设置模板文件”(如.gitignore),避免上传临时文件。...

jeecg-boot 源码项目-强烈推荐使用

JEECGBOOT低代码开发平台...

JetBrains推出全新AI编程工具Junie,强调以开发者为中心

IT之家2月1日消息,JetBrains发文,宣布推出一款名为Junie的全新AI编程工具,官方声称这款AI工具既能执行简单的代码生成与检查等基础任务,也能应对“编写测试、验证结...

JetBrains旗下WebStorm和Rider现已加入“非商用免费”阵营

IT之家10月25日消息,软件开发商JetBrains今日宣布,旗下WebStorm(JavaScript开发工具)和Rider(.NET开发工具)现已加入“非商用免费”阵营。如果...

谈谈websocket跨域

了解websocketwebsocket是HTML5的新特性,在客户端和服务端提供了一个基于TCP连接的双向通道。...

websocket调试工具

...

利用webSocket实现消息的实时推送

1.什么是webSocketwebSocket实现实现推送消息WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。以前的推送技术使用Ajax轮询,浏览器需...

Flutter UI自动化测试技术方案选型与探索

...

为 Go 开发的 WebSocket 库

#记录我的2024#...

「Java基础」Springboot+Websocket的实现后端数据实时推送

这篇文章主要就是实现这个功能,只演示一个基本的案例。使用的是websocket技术。...

【Spring Boot】WebSocket 的 6 种集成方式

介绍...

取消回复欢迎 发表评论: