目前已经在 ahooks 上提了 pr 预计 ahooks 4.0 会上;Issues链接
场景:
当在一些滚动场景,子元素设置
position: sticky时,我们并不能很容易的获取到这个粘性定位到元素是否处于一个Fixed的状态,这篇文章将封装一个useStickyFixed()的通用hook来解决这个场景
问问Google?
当尝试去搜索这个问题的答案时,绝大部分的方案都是通过 IntersectionObserverApi和top:-1px的解决方案
IntersctionObserver 是一个用来观察目标元素与其祖先元素(或视口)之间的交叉关系的API。
就是下面这种方案:
这种方案的原理是通过设置粘性定位元素 top:-1px, 当元素往上滚动时,将会在停止在屏幕外面1px处,那么此时 intersectionRaio就不是1了, 我们就知道元素已经fixed。
弊端
使用粘性定位比较多的同学会发现,这种方案复用性并不强
- IntersectionObserver是一个新的api,有可能会又出现兼容性问题。
- 如果我们的需求并不是在顶部fixed,而是在距离滚动区域100px的地方fixed,那么这里他们永远不会交叉。
探索
带着问题去寻找解决方案
- 不借助新api能不能实现。
- 如何做到较强的复用性(至少覆盖大部分场景)。
方案一
首先想到的就是在监听外层滚动事件,实时获取粘性定位元素到顶部距离来对比粘性定位元素的top值。当这两个值相等时就可以认定为当前处于fixed状态。
说干就干,最终这种方案确实可行,其中关键要做计算,这里不仅要考虑到吸顶还有就是要考虑吸底。
计算粘性定位元素到顶部距离来对比粘性定位元素的top值和粘性定位元素到底部距离来对比粘性定位元素的bottom值
这里的计算对比有一点比较复杂 通过dom元素拿到的粘性定位元素到顶部距离是以px为单位的,然而粘性定位元素的top值可以是 px、rpx、vw、vh、em、rem,这里还有做单位转化。
在实现过程中我发现一种更直观,实现更简单,不考虑单位的方案二。
方案二
当在滚动区域滚动时,会发现当处于fixed状态时,粘性定位元素相对屏幕是不动的,处于非fixed状态时,粘性定位元素相对屏幕一直都是在动的。通过这个只需要判断滚动方向就知道当前fixed状态是处于吸顶还是吸底,这种思路没有计算也不需要转化单位。
hooks版
这里未做方向判断,有需要的可自行判断
import { useRef } from "react";
import { getTargetElement, type BasicTarget } from "../utils/domTarget";
function useStickyFixed(
target: BasicTarget<Element>,
options?: {
scrollTarget?: BasicTarget<Element | Document>;
}
): boolean {
const { scrollTarget } = options || {};
const [state, setState] = useState<boolean>(false);
const lastTopRef = useRef(0);
useEffect(
() => {
const scrollElement = getTargetElement(scrollTarget, document);
if (!scrollElement) {
return;
}
const handleScroll = () => {
const stickyElement = getTargetElement(target);
if (!stickyElement) {
return;
}
const rect = stickyElement.getBoundingClientRect();
const currentTop = rect.top;
const lastTop = lastTopRef.current;
setState(currentTop === lastTop);
lastTopRef.current = currentTop;
};
scrollElement.addEventListener("scroll", handleScroll);
return () => {
scrollElement.removeEventListener("scroll", handleScroll);
};
},
[]
);
return state;
}
export default useStickyFixed;
使用事列
const ref = useRef()
const scrollRef = useRef()
const isFixed = useStickyFixed(target:ref,{scrollTarget:scrollRef})