React自定义Hook之useInView可视区检测

简介

本文主要讲述如何实现元素的可视区检测,并封装一个基本的React Hook,当项目中需要实现可视区检测相关的功能,例如监听元素是否进入视口、图片懒加载、动画触发等功能时,可以使用 useInView。本文将从原理出发,简要介绍 useInView 的实现,并从实际应用场景帮助读者理解如何使用它。

一、原理

1. Intersection Observer API

Intersection Observer 是浏览器提供的一个API,提供了一种异步检测目标元素与祖先元素或 视口(viewport) 相交情况变化的方法。

传统的可视区检测,一般采用监听滚动事件计算offsetTop、scrollTop或者频繁调用Element.getBoundingClientRect() 来获取元素的边界信息进行计算。 而 IntersectionObserver 提供了一种更高效的监听元素是否在当前视窗内的能力,下面我们一起来看下它的用法。

2. 用法

创建一个 IntersectionObserver 对象,并传入回调用函数和相应参数,该回调函数将会在目标 (target) 元素和根 (root) 元素的交集大小超过阈值 (threshold) 规定的大小时触发。

const options = {
  root: null, // 为null则默认为视口viewport
  rootMargin: "0px",
  threshold: 1, // 阈值为1,元素完全进入才触发回调
};

const observer = new IntersectionObserver(callback, options);

上面代码中,callback函数的参数接收两个参数entriesobserver

  • entries:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么entries数组里面就会打印出两个元素,如果只观察一个元素,我们打印entries[0]就能获取到被观察对象
  • observer: 这个是监视器本身

3. 应用场景:

(1)监听元素是否进入视口,上报访问次数,可用于数据埋点

let count = 0
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    // 元素可见
    if (entry.isIntersecting) {
        count = count + 1
        // 上报访问次数
        fetchReport(count)
    }
  });
}, options);
const target = document.getElementById('reportTarget')
observer.observe(target)

(2)无限滚动

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
        loadMore()
    }
  });
}, options);
const loader = document.getElementById('loader')
observer.observe(loader)

(3)图片懒加载

const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            const img = entry.target; 
            img.src = img.dataset.src; 
            observer.unobserve(img);
        } 
    });
});
const imgs = document.querySelectorAll('.img'); 
imgs.forEach((img) => {
    observer.observe(img);
});

4. Intersection Observer 优点

  • 异步触发:IntersectionObserver 观察器的优先级非常低,采用了requestIdleCallback(),不会占用主线程资源,只有在浏览器空闲下来才会执行观察器。
  • 减少开销:相比较传统的监听滚动事件, IntersectionObserver 避免了频繁的触发和计算,节省了资源。
  • 批量观察元素:可以一次性观察多个目标元素,以数组形式存储在callback函数的entries参数中,方便开发者进行批量操作。
  • 浏览器兼容性好(低版本使用参考 IntersectionObserver polyfill) image.png

二、基于Intersection Observer API封装useInView

1. useInView,直接上代码

import { useEffect, useState, useRef } from 'react';

const useInView = (
    options = {
        root: null,
        rootMargin: '0px 0px',
        threshold: 1,
    },
    triggerOnce = false, // 是否只触发一次
) => {
    const [inView, setInView] = useState(false);
    const targetRef = useRef(null);

    useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    setInView(true);
                    if (triggerOnce) {
                        // 触发一次后结束监听
                        observer.unobserve(entry.target);
                    }
                } else {
                    setInView(false);
                }
            });
        }, options);

        if (targetRef.current) {
            // 开始监听
            observer.observe(targetRef.current);
        }

        return () => {
            if (targetRef.current) {
                // 组件卸载时结束监听
                observer.unobserve(targetRef.current);
            }
        };
    }, [options, triggerOnce]);

    return [targetRef, inView];
};

export default useInView;

2. 使用示例

import React from 'react';
import useInView from './hooks/useInView';

const App = () => {
    const [targetRef, inView] = useInView();

    return (
        <div>
            <div style={{ height: '100vh' }}></div>
            <div
                ref={targetRef}
                style={{
                    width: '100px',
                    height: '100px',
                    background: inView ? 'green' : 'red',
                }}
            >
                {inView ? 'In View' : 'Out of View'}
            </div>
        </div>
    );
};

export default App;

3. ts版本

import { useEffect, useState, useRef } from 'react';
import type { MutableRefObject } from 'react';

type TargetRef = MutableRefObject<HTMLElement | null>;
const useInView = (
    options: IntersectionObserverInit = {
        root: null,
        rootMargin: '0px',
        threshold: 1,
    },
    triggerOnce: boolean = false,
): [TargetRef, boolean] => {
    const [inView, setInView] = useState(false);
    const targetRef: TargetRef = useRef<HTMLElement | null>(null);

    useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    setInView(true);
                    if (triggerOnce) {
                        observer.unobserve(entry.target);
                    }
                } else {
                    setInView(false);
                }
            });
        }, options);

        if (targetRef?.current) {
            observer.observe(targetRef.current);
        }

        return () => {
            if (targetRef?.current) {
                observer.unobserve(targetRef.current);
            }
        };
    }, [options, triggerOnce]);

    return [targetRef, inView];
};

export default useInView;

三、总结

useInView 基于 Intersection Observer API 进行封装,开发者可以更加轻松地实现视口检测相关的功能,如埋点数据上报、动画触发等。旨在为前端项目提供了更高效的能力,同时也能优化性能。无论你是在开发单页应用还是多页应用,都可以根据实际需求灵活运用useInView,从而为用户创造出更流畅的浏览体验。

全部评论

相关推荐

评论
点赞
收藏
分享
牛客网
牛客企业服务