判断粘性定位元素(position: sticky)是否已经固定。——自定义useStickyFixed()

620 阅读3分钟

目前已经在 ahooks 上提了 pr 预计 ahooks 4.0 会上;Issues链接

场景:

当在一些滚动场景,子元素设置position: sticky时,我们并不能很容易的获取到这个粘性定位到元素是否处于一个Fixed的状态,这篇文章将封装一个useStickyFixed()的通用hook来解决这个场景

问问Google?

当尝试去搜索这个问题的答案时,绝大部分的方案都是通过 IntersectionObserverApi和top:-1px的解决方案

IntersctionObserver 是一个用来观察目标元素与其祖先元素(或视口)之间的交叉关系的API。

就是下面这种方案:

这种方案的原理是通过设置粘性定位元素 top:-1px, 当元素往上滚动时,将会在停止在屏幕外面1px处,那么此时 intersectionRaio就不是1了, 我们就知道元素已经fixed。

弊端

使用粘性定位比较多的同学会发现,这种方案复用性并不强

  1. IntersectionObserver是一个新的api,有可能会又出现兼容性问题。
  2. 如果我们的需求并不是在顶部fixed,而是在距离滚动区域100px的地方fixed,那么这里他们永远不会交叉。

探索

带着问题去寻找解决方案

  1. 不借助新api能不能实现。
  2. 如何做到较强的复用性(至少覆盖大部分场景)。

方案一

首先想到的就是在监听外层滚动事件,实时获取粘性定位元素到顶部距离来对比粘性定位元素的top值。当这两个值相等时就可以认定为当前处于fixed状态。

说干就干,最终这种方案确实可行,其中关键要做计算,这里不仅要考虑到吸顶还有就是要考虑吸底

计算粘性定位元素到顶部距离来对比粘性定位元素的top值粘性定位元素到底部距离来对比粘性定位元素的bottom值

这里的计算对比有一点比较复杂 通过dom元素拿到的粘性定位元素到顶部距离是以px为单位的,然而粘性定位元素的top值可以是 px、rpx、vw、vh、em、rem,这里还有做单位转化。

在实现过程中我发现一种更直观,实现更简单,不考虑单位的方案二。

image.png

方案二

当在滚动区域滚动时,会发现当处于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})