Published on

useInViewport

Authors
  • avatar
    Name
    李丹秋
    Twitter

getTargetElement

源码

import type { MutableRefObject } from 'react';
import { isFunction } from './index';

type TargetValue<T> = T | undefined | null;

type TargetType = HTMLElement | Element | Window | Document;

export type BasicTarget<T extends TargetType = Element> =
  | (() => TargetValue<T>)
  | TargetValue<T>
  | MutableRefObject<TargetValue<T>>;

export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
    if (!target) {
        return defaultElement;
    }

    let targetElement: TargetValue<T>;

    if (isFunction(target)) {
        targetElement = target();
    } else if ('current' in target) {
        targetElement = target.current;
    } else {
        targetElement = target;
    }

    return targetElement;
}

  interface MutableRefObject<T> {
        current: T;
    }

getTargetElement这个函数的主要用途就是用函数、ref、element中,获取到element元素

createEffectWithTarget

源码

import type { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react';
import { useRef } from 'react';
import useUnmount from '../useUnmount';
import depsAreSame from './depsAreSame';
import type { BasicTarget } from './domTarget';
import { getTargetElement } from './domTarget';

const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
    /**
   *
   * @param effect
   * @param deps
   * @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
   */
    const useEffectWithTarget = (
        effect: EffectCallback,
        deps: DependencyList,
        target: BasicTarget<any> | BasicTarget<any>[],
    ) => {
        const hasInitRef = useRef(false);

        const lastElementRef = useRef<(Element | null)[]>([]);
        const lastDepsRef = useRef<DependencyList>([]);

        const unLoadRef = useRef<any>();

        useEffectType(() => {
            const targets = Array.isArray(target) ? target : [target];
            const els = targets.map((item) => getTargetElement(item));

            // init run
            if (!hasInitRef.current) {
                hasInitRef.current = true;
                lastElementRef.current = els;
                lastDepsRef.current = deps;

                unLoadRef.current = effect();
                return;
            }

            if (
                els.length !== lastElementRef.current.length ||
        !depsAreSame(els, lastElementRef.current) ||
        !depsAreSame(deps, lastDepsRef.current)
            ) {
                unLoadRef.current?.();

                lastElementRef.current = els;
                lastDepsRef.current = deps;
                unLoadRef.current = effect();
            }
        });

        useUnmount(() => {
            unLoadRef.current?.();
            // for react-refresh
            hasInitRef.current = false;
        });
    };

    return useEffectWithTarget;
};

export default createEffectWithTarget;

这个函数的effect执行,主要依赖于target和deps的变化,所以正常情况下只会执行一次,当dom发生改变的时候,会执行useEffect中相应的卸载操作。

useInViewport

源码

import 'intersection-observer';
import { useState } from 'react';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget';
import useEffectWithTarget from '../utils/useEffectWithTarget';

export interface Options {
  rootMargin?: string;
  threshold?: number | number[];
  root?: BasicTarget<Element>;
}

function useInViewport(target: BasicTarget, options?: Options) {
    const [state, setState] = useState<boolean>();
    const [ratio, setRatio] = useState<number>();

    useEffectWithTarget(
        () => {
            const el = getTargetElement(target);
            if (!el) {
                return;
            }

            const observer = new IntersectionObserver(
                (entries) => {
                    for (const entry of entries) {
                        setRatio(entry.intersectionRatio);
                        setState(entry.isIntersecting);
                    }
                },
                {
                    ...options,
                    root: getTargetElement(options?.root),
                },
            );
            // 检测目标元素
            observer.observe(el);

            return () => {
                observer.disconnect();
            };
        },
        [options?.rootMargin, options?.threshold],
        target,
    );

    return [state, ratio] as const;
}

export default useInViewport;

主要是使用了IntersectionObserver API去监听元素的位置变化情况

Intersection_Observer_API