这一次,彻底解决滚动穿透

什么是滚动穿透

如图所示,有一层遮罩蒙层覆盖在body上时,当我们滚动遮罩层,它下面的内容也会跟着一起滚动,看起来好像是上面的滚动事件穿透到下面的DOM元素上一样,我们称之为滚动穿透。

阻止冒泡?

刚开始遇到这个问题的同学可能会联想到是不是由于事件冒泡到body上引起的,于是监听 scroll/touchmove事件,阻止事件冒泡。

事实上,这并没有什么卵用。

首先,一般而言滚动不是我们自己监听事件去改变元素的位置而实现的,当我们设置 overflow:scroll/auto时,实际上是浏览器原生实现的滚动效果。

其次, scroll事件对于普通Element元素是不冒泡的,可参见MDN:

Bubbles Not on elements, but bubbles to the default view when fired on the document

所有的滚动都是在 Document上形成了一个 pending队列,然后按照一定的规则触发,详见W3C规范:

When asked to run the scroll steps for a Document doc, run these steps: For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps: If target is a Document, fire an event named scroll that bubbles at target. Otherwise, fire an event named scroll at target. Empty doc’s pending scroll event targets.

当我们滚动鼠标滚轮,或者滑动手机屏幕时,触发对象可分为两种类型(详见W3C规范):

  1. viewport被触发滚动, eventtarget为关联的 Document
  2. element元素被触发滚动,通常也就是我们添加 overflow滚动属性的element元素, eventtarget为相应的 node element

注意到这里,只有两种类型,当我们触发滚轮或滑动时,如果当前元素没有设置 overflow这样的属性,同时也没有 preventDefault掉原生的滚动/滑动事件,那么此时触发的是 viewport的滚动, position:fixed的元素并没有什么例外。

由此可见,滚动穿透问题其实并不是一个浏览器的bug(虽然在ios下fixed定位确实会导致很多bug),它是完全符合规范的,滚动的原则应该是 scrollforwhat can scroll,不应该因为某个元素的 CSS定位导致滚轮失效或者滑动失效。

然而对于我们的业务而言, ModalLayer元素触发整个 viewport滚动往往是一个bug,它会给用户带来非常不好的体验,因此我们需要去解决它。

加overflow:hidden?

既然它触发了整个 viewport的滚动,那么我们给 body上加个 overflow:hidden,让整个body变成不可滚动的元素:

html, body {    overflow: hidden;}

复制

这个想法很美好,在不侵入JS的情况下禁止滚动,然而:

只加 overflow:hidden对移动端是无效的!

当body的高度被内容撑开而滚动时,如果不对body的高度加以限制,只加入 overflow:hidden,此时在移动端依然可以滚动。

我们可以在加入 overflow:hidden的同时选择性做:

  1. 将 html,body的高度设置为 100%
  2. 将 html,body设置为绝对定位

这两个操作都可以完美地禁止整个body的滚动,但带来的最大问题是:

该方案会让浏览器的滚动条默认重置于初始位置

要解决这个问题,首先想到的方案是在添加 overflow之前,先记录当前浏览器的 scrollTop值,然后在添加之后重置 scrollTop,效果如下:

(请注意蒙层出现时,底部列表发生的变化)

在这个交互过程中,浮层弹出时,底部列表首先滚动条被置为初始态,关闭浮层后重置为之前的记录位置。实际上浮层的弹出背景是有一次跳变。

这种方案实现简单,若认为重置滚动条的跳变无伤大雅的情况下可以优先采用此方案。

阻止body的默认滚动?

直接阻止 documenttouchmove事件:

document.ontouchmove = e => {    e.preventDefault();};

复制

看起来好像非常严格,将整个页面的滚动全部禁止,但实践后发现:

该方案好像在Android中不生效?

这似乎颠覆了我们平时的认知,连 document的touchmove都禁不掉默认滚动?

在仔细进一步的定位下,最终确定罪魁祸首原来是:

passive event

复制

我们知道,chrome 51引入了 passiveeventlisteners以提高滚动性能,同时它也合入了标准,具体可查看chrome passive-event-listeners

简单介绍一下原理,就是我们监听 touchmove事件时,在之前是有一个小延迟触发的,因为浏览器不知道我们是否要 preventDefault,所以等到大概200ms左右才能真正收到监听回调。

chrome在56版本将 addEventListner默认的 passive置为true,具体请参见这里,这样浏览器就能知道这个 addEventListner是不用 preventDefault的,立即可触发滚动事件。

在Android的手q和微信中使用的是X5内核,它是基于blink内核的,因此同样有关于 passiveevent的优化。所以我们需要加入 addEventListner的第三个参数:

document.addEventListener(    'touchmove',    e => {        e.preventDefault();    },    { passive: false },);

复制

现在Android的手机也可以禁止掉浏览器的滚动了。当然 addEventListner的第三个参数是最新标准才更改为对象的,因此存在一些兼容性问题,我们需要做一个检测:

var supportsPassive = false;try {  var opts = Object.defineProperty({}, 'passive', {    get: function() {      supportsPassive = true;    }  });  window.addEventListener("test", null, opts);} catch (e) {}

复制

采用这种方案带来的最大的问题是:

所有的滚动事件全部被禁止了!

假如我们的浮层上真的需要滚动事件,就不能阻止这些元素的默认行为。

浮层上面的滚动元素?

既然浮层上面有需要滚动的元素,最简单的方案就是有选择性地阻止默认事件:

 document.addEventListener(  'touchmove',  e => {    const excludeEl = document.querySelectorAll('.can-scroll');    const isExclude = [].some.call(excludeEl, (el: HTMLElement) =>      el.contains(e.target),    );    if (isExclude) {      return true;    }    e.preventDefault();  },  { passive: false },);

复制

我们简单地规定带有 can-scroll类名的元素是可滚动的,这些元素以及他们的子元素全部采用不阻止默认事件策略。

这样一来只需要在可滚动的容器上加入 can-scroll类名即可滚动,但是这种滚动又随之带来一个问题:

当滚动到元素顶部和底部再继续滚动时,又会触发滚动穿透!

正如一开始介绍穿透问题那样,当滑动超出边界时,一样会触发默认的滚动穿透。对此,我们必须要在边界条件时阻止滚动:

// 监听所有可滚动元素的滚动事件[].forEach.call(scrollEl, (el: HTMLElement) => {  let initialY = 0;  el.addEventListener('touchstart', e => {    if (e.targetTouches.length === 1) {      // 单点滑动      initialY = e.targetTouches[0].clientY;    }  });
  el.addEventListener('touchmove', e => {    if (e.targetTouches.length === 1) {      // 单点滑动      const clientY = e.targetTouches[0].clientY - initialY;
      if (        el.scrollTop + el.clientHeight >= el.scrollHeight &&        clientY < 0      ) {        // 向下滑至底部        return e.preventDefault();      }      if (el.scrollTop <= 0 && clientY > 0) {        // 向上滑至顶部        return e.preventDefault();      }    }  });});

复制

经过这样的调整之后,看起来滚动穿透问题得到了完美的解决,但是:

当多个浮层同时存在时,滚动穿透将再次触发

支持多浮层

之所以会出现多浮层问题,是因为我们往 document上绑事件只绑一次,这个是对的,但是每个浮层关闭的时候都会触发 unbind,就会导致绑定的事件直接解绑,但其实这时还有其他浮层需要阻止滚动穿透。

解决办法也很简单,每一个浮层作为一个实例,我们定义一个Set来存储当前锁定的浮层:

const lockedList = new Set();lock() {  lockedList.add(this);  // 省略其他逻辑}
unlock() {  lockedList.delete(this);  if (lockedList.size <= 0) {    this.destroy();  }}

复制

只有当这个set没有值的时候,也就是所有的弹框均调用 unlock之后,再去解绑事件。

这样,整个方案就比较完美了:

更好的组件调用

经过一系列折腾,我们终于解决了问题,也给出了一个相对较为完美的解决方案。可是从使用性质来考虑,还不是很便捷,尤其是现在如 ReactVue这类框架中,还需要考虑浮层什么时候实例化,什么时候应当调用 lockunlock显得有些麻烦,因此编写了一个React版本的组件:

componentDidMount() {    const opts = this.props.selector      ? { selector: this.props.selector }      : undefined;    this.lockScroll = new LockScroll(opts);    this.updateScrollFix();}
updateScrollFix() {    const { lock } = this.props;    if (lock) {      this.lockScroll.lock();    } else {      this.lockScroll.unlock();    }}
componentDidUpdate(prevProps: ScrollFixProps) {    if (prevProps.lock !== this.props.lock) {      this.updateScrollFix();    }}
componentWillUnmount() {    console.log('scrollfix component will unmount!');    this.lockScroll.unlock();}

复制

思路也非常简单,组件传入一个 lock参数,当组件挂载时创建一个实例(保证了每个浮层一个实例),在lock变化时调用 lockunlock来解决滚动穿透,使用起来就非常简单了:

<ScrollFix lock={show}>  <!-- 浮层内容 --></ScrollFix>

复制

只需要将浮层包裹在组件内,并且传入 lock属性,即可不用再关注滚动穿透的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/416746.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Window系统禅道BUG管理系统安装配置并实现公网远程访问

文章目录 前言1. 本地安装配置BUG管理系统2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射本地服务3. 测试公网远程访问4. 配置固定二级子域名4.1 保留一个二级子域名5.1 配置二级子域名6. 使用固定二级子域名远程 前言 BUG管理软件,作为软件测试工程师的必备工具之一。在…

【Linux】进程信号 --- 信号的产生 保存 捕捉递达

文章目录 信号的感知信号的结构描述 一、信号的产生1.通过键盘发送信号2.通过系统调用发送信号 二、信号的保存&#xff08;PCB内部的两张位图和一个函数指针数组&#xff09;理解三张数据结构表block pending haldler 三、通过代码编写 理解 信号的保存和递达1.信号集操作的库…

看到递归就晕?带你理解递归的本质!【基础算法精讲 09】

104 . 二叉树的最大深度 链接 : . - 力扣&#xff08;LeetCode&#xff09; 思路 : 对于题意&#xff0c;可以拆分为 : ans max(左子树的最大深度 &#xff0c; 右子树的最大深度) 1 ; 原问题 : 计算整颗树的最大深度 &#xff1b; 子问题 : 计算左右子树的最大深度 ;…

Postgresql中dblink扩展的使用

一、介绍 Postgresql数据库提供了一个dblink扩展的插件&#xff0c;能够直接在一个数据库中操作另外一个远程数据库&#xff0c;比如&#xff1a;一个数据库在服务器A上&#xff0c;另外一个数据库在服务器B上&#xff0c;我可以在A这台服务器数据库上面建立一个到B服务器数据库…

Redis是单线程还是多线程?

说Redis是单线程或者是多线程这种说法并不严谨&#xff0c;要拿版本说话&#xff0c;Redis的版本有很多3.x、4.x和6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程是不太严谨的。 版本3.x&#xff0c;最早版本&#xff0c;此时Redis是单线程的版本4…

精品ssm人事办公考勤报销管理系统

《[含文档PPT源码等]精品基于ssm办公管理系统[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; Java——涉及技术&#xff1a; 前端使用技术&#xff1a;HTML5,CSS3、JavaS…

webrtc

stun服务 阿里云服务器安全组添加端口开放 webrtc-streamer视屏流服务器搭建 - 简书

安科瑞Acrel-2000ES 储能柜能量管理系统

安科瑞戴婷 安科瑞储能能量管理系统Acrel-2000ES&#xff0c;专门针对工商业储能柜、储能集装箱研发的一款储能EMS&#xff0c; 具有完善的储能监控与管理功能,涵盖了储能系统设备(PCS、BMS、电表、消防、空调等)的详细信息,实现了数据采集、数据处理、数据存储、数据查询与分…

浅谈 Linux 网络编程 - 网络字节序

文章目录 前言核心知识关于 小端法关于 大端法网络字节序的转换 函数 前言 在进行 socket 网络编程时&#xff0c;会用到字节流的转换函数、例如 inet_pton、htons 等&#xff0c;那么为什么要用到这些函数呢&#xff0c;本篇主要就是对这部分进行介绍。 核心知识 重点需要记…

4-如何进行细分市场的分析-02 细分行业的构成和基本情况

如何快速摸清行业的构成&#xff0c;通常会看同行或自己做过的相似的行业&#xff0c;会根据不同的行业来采用不同的研究方法。对于成熟的行业和不同的行业都会有一些比较通用的研究方式。 假设我们是在分析某一个行业&#xff0c;在分析行业的时候它的本质还是市场分析&#…

Leetcode300. 最长递增子序列 -代码随想录

题目&#xff1a; 代码(首刷看解析 2024年2月29日&#xff09;&#xff1a; class Solution { public:int lengthOfLIS(vector<int>& nums) {int n nums.size();if (n < 1) return 1;vector<int> dp(n, 1);int res 0;for (int i 1; i < n; i) {for(i…

springboot+vue实现oss文件存储

前提oss准备工作 进入阿里云官网&#xff1a;阿里云oss官网 注册 搜OSS&#xff0c;点击“对象存储OSS” 第一次进入需要开通&#xff0c;直接点击立即开通&#xff0c;到右上角AccessKey管理中创建AccessKey&#xff0c;并且记住自己的accessKeyId和accessKeySecret&#…

使用 Gradle 版本目录进行依赖管理 - Android

/ 前言 / 在软件开发中&#xff0c;依赖管理是一个至关重要的方面。合理的依赖版本控制有助于确保项目的稳定性、安全性和可维护性。 Gradle版本目录&#xff08;Version Catalogs&#xff09;是 Gradle 构建工具的一个强大功能&#xff0c;它为项目提供了一种集中管理依赖…

使用Python对数据进行rsa加密

#!/usr/bin/python3 import base64 import json import jsonpath import requests from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5 from base64 import b64decode, b64encodedef get_public_key():"""备注&#…

网络工程师笔记3

IP地址类型 A类 255.0.0.0B类 255.255.0.0C类 255.255.255.0D类 E类 子网掩码&#xff1a;从左到右连续的确定网络位 2-4-8-16-32-64-128-256 128 &#xff1a; 1000 0000 64 &#xff1a; 0100 0000 32 &#xff1a; 0010 0000 16 &#xff1a; 0001 0000 8 &am…

vue3 开发记录

1.引入nprogress插件&#xff0c;显示未声明文件 无法找到模块“nprogress”的声明文件。 解决方法&#xff1a; vite-env.d.ts // 解决引入模块的报错提示 declare module "nprogress";2.在 .evn 文件中创建了自定义环境变量 VITE_APP_BASE_URL 但在项目中使用时出…

【c语言】探索联合和枚举---解锁更多选择

前言 上一篇 讲解的是结构体相关知识&#xff0c;接着本篇主要讲解的是 联合和枚举 相关知识 结构体、联合体和枚举都属于 自定义类型。 那么接下来就跟上我的节奏&#xff0c;准备发车~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xf…

如何在群晖NAS中开启FTP服务并实现公网环境访问内网服务

文章目录 1. 群晖安装Cpolar2. 创建FTP公网地址3. 开启群晖FTP服务4. 群晖FTP远程连接5. 固定FTP公网地址6. 固定FTP地址连接 本文主要介绍如何在群晖NAS中开启FTP服务并结合cpolar内网穿透工具&#xff0c;实现使用固定公网地址远程访问群晖FTP服务实现文件上传下载。 Cpolar内…

同局域网共享虚拟机(VMware)

一、前言 首先我们先来了解下 VMware 的三种网络模式桥接模式、NAT模式、仅主机模式&#xff0c;网络类型介绍详情可以参考下我之前的文档 Linux系统虚拟机安装&#xff08;上&#xff09;第三章 - 第9步指定网络类型。了解三种网络模式的原理之后&#xff0c;再来剖析下需求&…

Python进阶学习:axis=0和axis=1的区别和用法

Python进阶学习&#xff1a;axis0和axis1的区别和用法 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的订阅和…