封装一个监听scroll事件的组件

342 阅读3分钟

前言

应用场景:

  1. 我们经常见到的在一些软件初始页面的条约申明阅读,用户必须要浏览完这页的申明才能进行下一步,这里面就会检测这元素是否有滑动到底部。
  2. 根据下拉还是下拉去实现其他的功能(如:下拉时标题需要消失,上拉时显示)等等

根据上面的场景,尝试封装一个scroll组件及hook。

知识回顾

开始前,我们需要回顾一下元素样式的四个属性,可以自行查阅更多相关的属性。

offsetHeight:一个元素能够展示其所有内容所需要的最小高度,是元素整个的content加上padding的高度,不包括border。如果元素内容超过可视区域,可以想象成将整个元素撑开的高度。

scrollHeight:一个元素的content+padding+border+margin+scroll bar的高度。也是在可视范围内这些高度的相加。

offsetTop:当前对象到其上级层顶部的距离。

scrollTop:对象的最顶部到对象在当前窗体显示的范围内的顶边的距离 即是在出现了纵向滚动栏的情况下,滚动栏拉动的距离。

React操作Dom的几种方式:传入字符串传入一个对象(react推荐的方式),传入一个函数。今天这里使用react封装过的高阶组件forwardRef来操作DOM

分析

组件:

实现

type Props = React.PropsWithChildren<{  
  onScrolled(direction: 'up' | 'down'): void;
  toTop?:any
}>;

const ScrollWrap: ForwardRefRenderFunction<HTMLDivElement, Props> = (props, ref: any = {}) => {
  const { children, toTop, onScrolled } = props;
  const {
    locale: { carddetail },
    lang
  } = useContext(LocaleContext);

  useScroll(ref.current, onScrolled);
  useEffect(() => {
    const $root = $(ref.current);
    $root.animate({ scrollTop: 0 });
  }, [toTop]);

  return (
    <div className={classes.root} div-mark={'scrollmark'} ref={ref}>
      {children}
    </div>
  );
};

export default React.memo(forwardRef(ScrollWrap));

如上,children相当于插槽,然后通过ref拿到实例。通过是否有toTop的的更新来判断是否要让滚动条回到顶部。

主要逻辑:useScroll

type DomPositions = [number, number, number, number]; //offsetTop,offsetHeight,scrollTop,scrollHeight;

const positionChanged = (before: DomPositions, after: DomPositions) => {
  //如果offsetHeight + scrollTop 的值不变,说明scrollBottom没变,则认为position没有变
  //如果offsetHeight + scrollTop = scrollHeight 说明此时页面已经拉到了底部

  //1.判断是否拉到了底部 如果是 则默认没有动 如果不是 则根据scrollTop判断
  //return 0 不动,1下拉 -1上拉
  if (Math.abs(after[1] + after[2] - after[3]) < 10) {
    return 0;
  }
  return after[2] - before[2] > 0 ? -1 : 1;
};
const useScroll = (dom: any, onScrolled?: (direction: 'up' | 'down') => void) => {
  /**
   * 记录此刻状态 offsetTop,scrollTop,offsetHeight,scrollHeight;
   */
  const [domPosition, setDomPosition] = useState<DomPositions>([0, 0, 0, 0]);
  const [scrollDir, setScrollDir] = useState<API.Components.CardDetailLayoutContent.Content.ScrollDirection>('up');
  const debounced = useDebounce(scrollDir, 200);

  useEffect(() => {
    onScrolled && onScrolled(debounced);
  }, [debounced, onScrolled]);

  useEffect(() => {
    const myFunc = (ev: any) => {
      const { offsetTop, scrollHeight, scrollTop, offsetHeight } = ev?.target;
      const newPosition: DomPositions = [offsetTop, offsetHeight, scrollTop, scrollHeight];
      const pchange = positionChanged(domPosition, newPosition);

      if (pchange !== 0) {
        setScrollDir(pchange === 1 ? 'up' : 'down');
      }
      setDomPosition(newPosition);
    };

    const throttleFunc = throttle(myFunc, 500, 1000, false);
    dom?.addEventListener && dom.addEventListener('scroll', throttleFunc);

    return () => {
      dom?.removeEventListener && dom.removeEventListener('scroll', throttleFunc);
    };
  }, [dom, domPosition]);
};

必不可少的节流:鼠标移入能立刻执行,停止触发的时候还能再执行一次

function throttle(fun, t, mustRun, denyLast) {
  let timer = null;
  let startTime = 0;
  return function(event) {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    let that = this;
    // eslint-disable-next-line prefer-rest-params
    let args = arguments;
    clearTimeout(timer);
    let later = function() {
      timer = null;
      if (denyLast) fun.apply(that, args);
      console.log('执行的是later.');
    };
    let currTime = new Date().getTime();
    if (currTime - startTime >= mustRun) {
      fun.apply(that, args);
      startTime = currTime;
    } else {
      timer = setTimeout(later, t);
    }
  };
}
export default throttle;