You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中document.querySelectorAll等效实现及滚动时多Ref监听方案

在React中实现document.querySelectorAll('.classname')的等效方案,以及滚动监听多个Ref的实践

嘿,刚好之前做过类似的视口动画需求,我来一步步给你拆解解决思路:

首先,在React里我们确实应该尽量避免直接用document.querySelectorAll这类原生DOM查询——因为React的虚拟DOM会管理元素的渲染,直接操作DOM容易和React的状态更新冲突。替代方案就是用Refs,但多个元素的话需要用「Ref数组/集合」来存储它们的引用。


第一步:收集多个元素的Ref

你可以用useRef初始化一个空数组,然后通过每个<li>的ref回调函数,把对应的DOM元素添加到数组里。注意要处理组件卸载或者重新渲染的情况,避免重复添加元素:

import { useRef, useEffect } from 'react';

function ScrollAnimationList() {
  // 初始化一个Ref数组,用来存储所有<li>元素
  const itemRefs = useRef([]);

  // 渲染<li>时,通过ref回调收集元素
  const setItemRef = (element) => {
    if (element && !itemRefs.current.includes(element)) {
      itemRefs.current.push(element);
    }
    // 可选:组件卸载时移除元素,避免内存泄漏
    return () => {
      itemRefs.current = itemRefs.current.filter(el => el !== element);
    };
  };

  return (
    <ul>
      <li ref={setItemRef} data-position="below-viewport">Item 1</li>
      <li ref={setItemRef} data-position="below-viewport">Item 2</li>
      <li ref={setItemRef} data-position="below-viewport">Item 3</li>
      {/* 更多列表项... */}
    </ul>
  );
}

第二步:在滚动事件中监听多个元素的视口状态

接下来要给窗口添加滚动监听,然后遍历所有收集到的Ref元素,判断它们是否进入了视口。这里我们需要一个检测元素是否在视口的工具函数,然后结合节流来优化性能(因为滚动事件触发非常频繁):

// 检测元素是否在视口内的工具函数
const isInViewport = (element) => {
  if (!element) return false;
  const rect = element.getBoundingClientRect();
  // 这里可以根据需求调整判断条件,比如元素完全进入视口才触发
  return (
    rect.top <= window.innerHeight &&
    rect.bottom >= 0
  );
};

// 简单的节流函数(也可以用lodash.throttle)
const throttle = (func, delay) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      func(...args);
      lastCall = now;
    }
  };
};

function ScrollAnimationList() {
  const itemRefs = useRef([]);

  const setItemRef = (element) => {
    if (element && !itemRefs.current.includes(element)) {
      itemRefs.current.push(element);
    }
    return () => {
      itemRefs.current = itemRefs.current.filter(el => el !== element);
    };
  };

  useEffect(() => {
    // 节流后的滚动处理函数
    const handleScroll = throttle(() => {
      itemRefs.current.forEach(item => {
        const isVisible = isInViewport(item);
        // 更新data-position属性,触发CSS动画
        if (isVisible && item.dataset.position === 'below-viewport') {
          item.dataset.position = 'in-viewport';
        }
        // 可选:元素移出视口时重置状态
        // else if (!isVisible && item.dataset.position === 'in-viewport') {
        //   item.dataset.position = 'below-viewport';
        // }
      });
    }, 100); // 100ms节流间隔,可根据需求调整

    // 添加滚动监听
    window.addEventListener('scroll', handleScroll);
    // 组件卸载时移除监听
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <ul>
      <li ref={setItemRef} data-position="below-viewport">Item 1</li>
      <li ref={setItemRef} data-position="below-viewport">Item 2</li>
      <li ref={setItemRef} data-position="below-viewport">Item 3</li>
    </ul>
  );
}

更「React式」的优化:用状态管理元素状态

上面的代码直接修改了dataset属性,虽然能工作,但更符合React思想的方式是用状态来控制每个元素的类名或状态,而不是直接操作DOM。比如可以用一个数组状态来存储每个元素的可见性:

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

function ScrollAnimationList() {
  const itemRefs = useRef([]);
  // 用状态存储每个元素的可见性,初始都是false
  const [itemVisibilities, setItemVisibilities] = useState([false, false, false]);

  const setItemRef = (element, index) => {
    if (element) {
      itemRefs.current[index] = element;
    }
  };

  useEffect(() => {
    const handleScroll = throttle(() => {
      const newVisibilities = itemVisibilities.map((_, index) => {
        const item = itemRefs.current[index];
        return item ? isInViewport(item) : false;
      });
      setItemVisibilities(newVisibilities);
    }, 100);

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [itemVisibilities]);

  return (
    <ul>
      {itemVisibilities.map((isVisible, index) => (
        <li
          key={index}
          ref={(el) => setItemRef(el, index)}
          className={isVisible ? 'in-viewport' : 'below-viewport'}
        >
          Item {index + 1}
        </li>
      ))}
    </ul>
  );
}

然后你的CSS就可以这样写:

.below-viewport {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.in-viewport {
  opacity: 1;
  transform: translateY(0);
}

注意事项

  • 动态列表处理:如果你的列表是从API动态获取的,要确保itemRefs和itemVisibilities的长度和列表项一致,避免索引错误。
  • 性能优化:除了节流,还可以用Intersection Observer API替代滚动监听,它是浏览器原生的性能更优的视口检测方案,尤其适合大量元素的场景。
  • Ref清理:一定要在组件卸载时清理滚动监听和Ref数组里的元素,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Giovanni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:34:16