【问题标题】:I have a counter which has to be triggered only once when the page scrolls to certain element in react?我有一个计数器,当页面滚动到某个元素时必须触发一次反应?
【发布时间】:2025-11-23 07:55:02
【问题描述】:

我在页面的某个部分有一个计数器,当滚动到达该元素时,它只需要触发一次。我可以在 jQuery 中看到各种解决方案,但我正在寻找 react 或纯 javascript 方面的答案。

我使用状态和反应钩子实现了它,但即使我保留状态并使用条件语句,我也看到一些不稳定性一直触发不止一次。我不确定我哪里出错了。

这里是 react 类组件(由于使用 useEffect 时不断警告,我从函数切换到类)

更新:我找到了一个可行的解决方案,如下所示。如果有任何其他更好的方法可以做到这一点,请告诉我。

import React, { useState, useCallback, useEffect } from "react";

export default function DataCounter({ count, title }) {
  const [number, setNumber] = useState(0);
  const [trigger, setTrigger] = useState(false);

  const increment = useCallback(() => {
    const counter = (length = 2000) => {
      setNumber(0);
      let n = count;
      let start = Date.now();
      let end = start + length;
      let interval = length / n;
      const sInt = setInterval(() => {
        let time = Date.now();
        if (time < end) {
          let count = Math.floor((time - start) / interval);
          setNumber(count);
        } else {
          setNumber(n);
          clearInterval(sInt);
        }
      }, interval);
    };

    counter();
  }, [count]);

  document.addEventListener("scroll", async () => {
    const element = await document.getElementById("data");
    const elementPosition = await element.getBoundingClientRect().top;

    if (window.pageYOffset > elementPosition) setTrigger(true);
  });

  useEffect(() => {
    if (trigger) {
      increment();
    }
  }, [trigger, increment]);

  return (
    <div className="counter">
      <div className="counter-number">{number}</div>
      <p className="counter-title">{title}</p>
    </div>
  );
}


初始版本:

import React from "react";

export default class Counter extends React.Component {
   state = {
    number: 0,
    loaded: false
  };

  increment(n, length = 2000){
    this.setState({ number: 0 });

    let start = Date.now();
    let end = start + length;
    let interval = length / n;
    const sInt = setInterval(() => {
      let time = Date.now();
      if (time < end) {
        let count = Math.floor((time - start) / interval);
        this.setState({ number: count });
      } else {
        this.setState({ number: n });
 
        clearInterval(sInt);
      }
    }, interval);
  };

  async handleScroll() {
    const element = await document.getElementById("data");
    const elementPosition = await element.getBoundingClientRect().bottom;

    if (window.pageYOffset > elementPosition) {
      if(!this.state.loaded) {
        this.increment(100);
        this.setState({ loaded: true });
      }
      return;
    }
  }

  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  render() {
    return (
      <div className="counter">
        <div className="counter-number">{this.state.number}</div>
        <p className="counter-title">{this.props.title}</p>
      </div>
    );
  }
}

【问题讨论】:

  • 我明白这一点:我在页面的某个部分有一个计数器,当滚动到达该元素时只能触发一次,但是你的 @987654323 @函数到底是干什么的?
  • 它在 2 秒内将数字从 0 增加到 100,在这种情况下,我将值硬编码为 100。每当我到达页面上的特定元素时,我想触发这个 increment
  • 你能把你的功能组件和你的原始代码分开吗?覆盖让我的答案看起来很混乱
  • 你的回答是对的。我缺少的是this。此外,我添加了async 以等待元素被加载,因为可以在组件完全加载之前完成滚动。谢谢。

标签: javascript reactjs counter


【解决方案1】:

首先,您以错误的方式声明了您的类方法。由于您使用 ES6 方式声明状态(您没有使用构造函数),因此您也必须以 ES6 方式声明您的类方法。比如,

increment = (n, length = 2000) => {
   // so you can access *this* variable in this scope
   ...
}

handleScroll 方法也是如此。如果您想像在代码中那样声明一个辅助方法:

increment(n, length = 2000){
  ...
}

那么你需要声明一个构造函数,并在构造函数中绑定this你可以找到引用here

其次,绝对没有必要将handleScroll 作为异步函数。此方法中没有返回任何承诺。

最后但并非最不重要的一点,始终记得删除componentWillUnmount 中的滚动监听器,这样当用户在路由之间切换或组件再次挂载时,您将不会有一堆监听器监听 windows 滚动事件。

这里是a working example on codesandbox

【讨论】:

    【解决方案2】:

    我发现你的代码有问题

    您正在函数组件的主体内注册“滚动”事件侦听器。这就是滚动事件被多次触发的原因。

    因为函数组件的每次渲染都会向 DOM 添加一个额外的滚动事件

    注册和清理任何 DOM 事件的正确方法是在 'useEffect' 反应钩子中,像这样

    const handleScrollEvent = async () => {
        const element = await document.getElementById("data");
        const elementPosition = await element.getBoundingClientRect().top;
    
        if (window.pageYOffset > elementPosition) setTrigger(true);
    }
    
    const registerEvent = () => {
        document.addEventListener("scroll", handleScrollEvent);
    }
    
    const unRegisterEvent = () => {
        document.removeEventListener("scroll", handleScrollEvent);
    }
    
    useEffect(() => {
        if (trigger) {
            increment();
        }
    
        //Register the event inside useEffect after the component mounts
        registerEvent();
    
        //Cleanup the registered event once the component is unmounted from 
        //the DOM
        return unRegisterEvent;
    
    }, [trigger, increment]);
    

    【讨论】:

    • 我有一个问题。每当我向 useEffect 添加任何需要在组件安装后调用的函数时,我总是看到警告,将函数添加到依赖项列表中,也许这就是我将事件侦听器保留在 @987654324 之外的原因之一@。如果我将该函数添加到依赖项列表中,则会显示另一个警告以在useCallback 中添加该函数。那么解决方案是什么,即使对于这个解决方案,它也会为useEffect 中的两个添加函数显示警告(红色下划线)。
    • @hashBender,我们可以忽略这种情况下的警告。只希望在依赖项列表中添加组件的状态值。添加局部变量可能会导致意外行为,例如无限重新渲染
    • 更多依赖列表请参考medium.com/better-programming/…
    • 在上面的链接中,有一个主题“什么是依赖数组?”。它会帮助你理解
    最近更新 更多