css中有一类滚动继承问题。假如现在有一个淘宝商品页,商品列表可以滚动,列表上面有一个筛选按钮,点击可弹出一个筛选列表:

假如图中黄色框选区域可以滚动,当滚动到底部边缘再继续滚动时,会发现商品列表竟然开始滚动了,怎么会是呢?
首先可以先看一下这篇文章:CSS overscroll-behavior让滚动嵌套时父滚动不触发,同时里面有作者对这一问题的处理方法。这个问题主要是在写移动端页面的时候会遇到,到目前为止,safari浏览器还是不支持overscroll-behavior属性,所以没法通过设置这个属性解决。我目前的解决办法是在vant的源码中找到的。vant里有一个下拉菜单组件:

这里面也涉及了滚动继承问题,vant做了相应处理。
以下代码为Vue2.x下的代码:
目录结构:
- views
-- demo.vue
- mixins
-- touch.js
-- overscroll
--- index.js
- utils
-- dom
--- event.js
--- scroll.js
scroll.js文件内容:
// get nearest scroll element
// https://github.com/youzan/vant/issues/3823
/**
* 获取最近触发滚动事件的元素
* refer to https://github.com/youzan/vant/blob/v2.12.15/src/utils/dom/scroll.ts
* @param {Element} el 当前元素
* @param {Element | Window} root 根元素
* @returns {Element | Window}
*/
const overflowScrollReg = /scroll|auto/i;
export function getScroller(el, root = window) {
let node = el;
while (
node &&
node.tagName !== "HTML" &&
node.tagName !== "BODY" &&
node.nodeType === 1 &&
node !== root
) {
const { overflowY } = window.getComputedStyle(node);
if (overflowScrollReg.test(overflowY)) {
return node;
}
node = node.parentNode;
}
return root;
}
event.js文件内容
/**
* 阻止事件冒泡
* @param {Event} event
* @returns {void}
*/
export function stopPropagation(event) {
event.stopPropagation();
}
/**
* 阻止默认事件
* refer to https://github.com/youzan/vant/blob/v2.12.15/src/utils/dom/event.ts
* @param {Event} event
* @param {boolean} isStopPropagation
* @returns {void}
*/
export function preventDefault(event, isStopPropagation) {
if (typeof event.cancelable !== "boolean" || event.cancelable) {
event.preventDefault();
}
if (isStopPropagation) {
stopPropagation(event);
}
}
touch.js文件内容:
/**
* @description touch事件混入
* refer to https://github.com/youzan/vant/blob/v2.12.15/src/mixins/touch.js
*/
const MIN_DISTANCE = 10;
function getDirection(x, y) {
if (x > y && x > MIN_DISTANCE) {
return "horizontal";
}
if (y > x && y > MIN_DISTANCE) {
return "vertical";
}
return "";
}
export const touch = {
data() {
return {
direction: "", // 移动方向 horizontal或vertical
startX: 0, // touchstart X值
startY: 0, // touchstart Y值
deltaX: 0, // touchmove X轴差值 带符号
deltaY: 0, // touchmove Y轴差值 带符号
offsetX: 0, // touchmove X轴偏移量
offsetY: 0 // touchmove Y轴偏移量
};
},
methods: {
touchStart(event) {
this.resetTouchStatus();
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
},
touchMove(event) {
const touch = event.touches[0];
// Fix: Safari back will set clientX to negative number
this.deltaX = touch.clientX < 0 ? 0 : touch.clientX - this.startX;
this.deltaY = touch.clientY - this.startY;
this.offsetX = Math.abs(this.deltaX);
this.offsetY = Math.abs(this.deltaY);
this.direction =
this.direction || getDirection(this.offsetX, this.offsetY);
},
resetTouchStatus() {
this.direction = "";
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
this.startX = 0;
this.startY = 0;
}
}
};
overscroll/index.js文件内容:
/**
* @description 阻止滚动继承,类似overscroll-behavior: contain
* refer to https://github.com/youzan/vant/blob/v2.12.15/src/mixins/popup/index.js
*/
import { getScroller } from "@/utils/dom/scroll";
import { preventDefault } from "@/utils/dom/event";
import { touch } from "../touch";
export const OverscrollMixin = {
mixins: [touch],
methods: {
onTouchMove(event) {
this.touchMove(event);
// '10'-下拉 '01'-上拉
const direction = this.deltaY > 0 ? "10" : "01";
const el = getScroller(event.target, this.$el);
const { scrollHeight, offsetHeight, scrollTop } = el;
let status = "11";
if (scrollTop === 0) {
// 处于顶部
// offsetHeight>=scrollHeight 说明元素不可滚动
// '00'-元素不可滚动 '01'-元素可滚动
status = offsetHeight >= scrollHeight ? "00" : "01";
} else if (scrollTop + offsetHeight >= scrollHeight - 1) {
// 由于offsetHeight为小数,因此可能会出现scrollTop + offsetHeight略小于scrollHeight的情况,因此-1
// 处于底部
status = "10";
}
if (
status !== "11" &&
this.direction === "vertical" &&
!(parseInt(status, 2) & parseInt(direction, 2))
) {
// 元素不处于顶部或底部
// 且滚动方向为垂直
// 且向上拉拉到底部了或向下拉拉到顶部了
preventDefault(event, true);
}
},
},
};
最后,在demo.vue中引入overscroll/index.js作为混入:
import { OverscrollMixin } from "@/mixins/overscroll";
export default {
mixins: [OverscrollMixin]
}
在你不想要滚动继承的元素上添加事件绑定函数,这个元素本身需要是可滚动的,即overflow-y需要是scroll或auto。假设在demo.vue中可滚动元素类名为"content":
<template>
<div class="page_container">
...
<div ref="content" class="content">
</div>
...
</div>
</template>
需要在mounted生命周期中为content绑定touchstart和touchmove事件:
export default {
...
mounted() {
let content = this.$refs.content;
content.addEventListener("touchstart", this.touchStart, {
passive: false,
});
content.addEventListener("touchmove", this.onTouchMove, {
passive: false,
});
},
...
}
touchStart和onTouchMove方法都是在OverscrollMixin里混入来的,当然OverscrollMixin本身也混入了其他混入。以上代码如果用vue3的composition api来写会更加清晰。
如果你的页面加了keep-alive,可以将上面mounted里的方法写在activated里,同时在deactiveted里也要移除一下事件监听。
这个方法通过监听touchstart和touchmove方法,记录手指划过的方向,同时寻找当前点击元素最近的父代可滚动元素,计算是否滚动到顶部或底部,如果滚动到顶部或底部则阻止默认事件,防止滚动继承的发生。
这里的代码其实还能再写得好些,不过先这样吧。
网友评论