小东西快快学快快记,大知识按计划学,不拖延
滚动穿透相信大家平常开发的时候也经常遇到,网上也有很多解决办法
今天我就谈下我对 滚动穿透的理解 和 总结下我们大佬写的一个比较完美的解决方案
不废话,本文分为3部分
1、什么是滚动穿透
2、为什么会滚动穿透
3、怎么解决滚动穿透
4、碰到的问题
什么是滚动穿透
大家肯定不陌生了,做移动端开发的,肯定都碰到过,比如 我明明滚动的是弹窗,但是底下的 document 却在滚动
不说这么多,直接看
为什么会滚动穿透
首先,这不是一个bug,这是一个合理且正常的表现
阅读了官方的文档之后,我也是理解了好久
https://www.w3.org/TR/cssom-view/#scrolling
以下是个人的理解
当用户开始滚动的时候,页面响应滚动有两种类型
1、document 滚动
2、可滚动 element 滚动
只有两种类型,就是说,一旦有滚动行为发生,那么就必然产生这两个类型其中之一
如果 element 可以滚动,那么就 滚动 element
如果 element 无法滚动,那么就让 document 响应滚动
是一个 if-else 的关系
这个element 无法滚动包括
- 没有设置可滚动overflow属性
- 监听回调 设置了 preventDefault
- 已经滚动到底端或顶端
为什么会觉得这个这个行为是合理性,我的理解是
用户产生滚动行为,浏览器就必须要响应这个行为,产生滚动的反馈,这才是正常的。尽可能响应,滚动一切当前操作可以滚动的元素
只是当把元素设置了 fixed 之后让人感觉是个bug,浏览器没有必要对 fixed 元素做特殊处理,两个不相关的东西,不可能耦合起来
怎么解决滚动穿透
我们理解了滚动穿透的原因之后,我们就可以对症下药了
既然 document 是备胎滚动选项,那么就让 document 不可滚动
1body overflow hidden
代码语言:javascript复制html, body { overflow: hidden; }
PC 可以,但是对移动端无效
那么我们限制body不超过一屏,那么自然就不能滚动了?
2body height 100%
代码语言:javascript复制html, body { overflow: hidden; height:100%}
是可以,但是会丢失 滚动高度,文档回到最顶部。体验不好
3记录滚动高度,弹窗关闭重新赋值
既然丢失滚动高度,那么就记录下滚动高度 scrollTop ?然后关闭弹窗的时候再赋值回去?
页面内容从 0 突然跳到 原先位置,可想而知会有 闪动,体验仍然不好
4避免页面跳回顶部
拿到 页面的滚动高度,在给 html 设置 这些样式的时候
代码语言:javascript复制html{ overflow: hidden; height:100%}
在设置 absolute,top 设置成之前拿到滚动高度(伪代码)
代码语言:javascript复制html {
position:absolute;
top: scrollTop
}
利用这种方式保证内容处在同一位置,这样就可以避免页面的跳动,但是直接给 html 设置 absolute 风险太大,容易埋坑,不太建议大项目使用,小应用还是可以的,我在需求的小活动页7就使用过这种方式
5禁用页面滚动
除了在 css 限制页面滚动,还可以从 js 去限制
代码语言:javascript复制document.addEventListener( 'touchmove', e => e.preventDefault());
这里要注意一个问题,在 chrome51 中在监听回调更新了参数,如果你不加上这个参数,那么可能这样并不能禁用页面滚动
具体如下
以前 addEventlisener 参数 是
代码语言:javascript复制target.addEventListener(type, listener[, useCapture]);
第三个参数是 控制监听器是 捕获阶段还是 冒泡阶段执行,默认值是 false(冒泡阶段执行)
现在变成了
代码语言:javascript复制target.addEventListener(type, listener[, options]);
第三个参数变成了对象,包含一个属性 passive
这个参数主要是为了提高滚动流畅度
因为在一开始的时候,浏览器响应滚动 大概会有 200ms 的延迟
因为浏览器不知道监听的回调是否调用了 preventDefault 来取消滚动
所以只好等回调执行完毕,大概 200ms 后, 页面再开始响应滚动,所以会显得不那么跟手
现在通过 参数 passive 就可以事先告诉浏览器 这个监听回调不会 执行 preventDefault,你可以马上响应滚动不用等待
从而 提升了滚动的流畅度
但是 passive 是新出的标准,但是以前没有,所以我们需要做一个兼容
代码语言:javascript复制var options = false;
window.addEventListener("test", null, {
get passive() {
options = { passive: true };
return undefined;
},
});
elem.addEventListener("touchstart", fn, options);
具体可以看下 justjavac 写的文章
https://zhuanlan.zhihu.com/p/24555031
所以我们禁用页面滚动,可能得这么写,告诉浏览器我们需要禁用滚动
代码语言:javascript复制document.addEventListener(
'touchstart',
e => e.preventDefault(),
{ passive: false}
);
但是这样就会把页面所有滚动都禁止
所以我们需要开放一个白名单,当滚动的元素在白名单之内,我们就放开限制
这个白名单的设置就是 给元素加上 can-scroll 类名,这样就可以放开滚动
代码语言:javascript复制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 }
);
但是对待白名单的元素放开限制之后,当元素滚动到顶部和底部的时候,再滚动,仍然会触发document 滚动
为什么呢?
之前我们说了,浏览器需要尽可能响应滚动行为,element 滚到两端 element 滚不了,那我就滚 document
所以我们最好监听 element 滚到 顶部和 底部的时机,继续禁止滚动行为
代码语言:javascript复制var initialY = 0;
el.ontouchstart = function (e) {
if (e.targetTouches.length === 1) {
// 单点滑动
initialY = e.targetTouches[0].clientY;
}
};
el.ontouchmove = function (e) {
if (e.targetTouches.length === 1) {
// 单点滑动
var 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();
}
}
};
碰到的问题
1父子元素也存在滚动穿透
这个问题测试了,只在 ios 中存在,滚动穿透的顺序是 子->父->document,而 安卓和 鸿蒙 则不会,子滚不了,直接滚document
这个是实际的dom 父子关系才会,视觉上的 父子关系没有这个问题
2子元素 e.stopPropagation() 会让 preventDefault 失效
比如这样
代码语言:javascript复制document.addEventListener(
"touchmove",
(e) => {
e.preventDefault();
},
{ passive: false }
);
document.querySelector(".modal").addEventListener("touchmove", (e) => {
e.stopPropagation();
});
虽然document 取消了默认事件,本来整个页面都不能滚了
但是子元素 调用了 stopPropagation() 之后,不仅元素可以滚了,还会导致滚动穿透(毕竟只要元素能滚就能发生穿透)
但是document 还是不会滚动的
3滚动穿透的触发条件
一次没有抬起的滚动行为(手没有离开屏幕)导致元素滚动到顶部或者 底部之后,如果手还在屏幕上往两端滑,并不会触发滚动穿透
如果你把元素滚动到 两端不可滚之后,抬起手,再按下去,往不可滚的方向移动,此时才会发生 滚动穿透
之前我们说了,滚动响应有两种对象,element 和 document
从这里可以意识到,单次的滚动行为 只会绑定一个滚动对象,不会切换响应对象
只是在开始滚动的时候,浏览器会根据情况,选择响应滚动的对象,选择时候不会切换