【问题标题】:React setState hook from scroll event listener从滚动事件侦听器反应 setState 钩子
【发布时间】:2019-11-27 01:19:59
【问题描述】:

首先,使用类组件,这可以正常工作并且不会引起任何问题。

但是,在带有钩子的功能组件中,每当我尝试从滚动事件侦听器的函数 handleScroll 设置状态时,即使我使用 debounce,我的状态也无法更新或应用程序的性能会受到严重影响。

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

let prevScrollY = 0;

const App = () => {
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY < currentScrollY && goingUp) {
      debounce(() => {
        setGoingUp(false);
      }, 1000);
    }
    if (prevScrollY > currentScrollY && !goingUp) {
      debounce(() => {
        setGoingUp(true);
      }, 1000);
    }

    prevScrollY = currentScrollY;
    console.log(goingUp, currentScrollY);
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

尝试在handleScroll 函数中使用useCallback 挂钩,但没有多大帮助。

我做错了什么?如何在不影响性能的情况下从 handleScroll 设置状态?

我已经为这个问题创建了一个沙盒。

【问题讨论】:

  • 您仍在为每一次去抖动创建句柄滚动。你应该记住它。您的卸载仍在调用addEventListener 而不是removeEventListener。这两者都可能导致问题:)
  • @JohnRuddell 嘿,感谢您在 removeEventListener 上发现错误。但是,这会导致内存泄漏,但与我要解决的性能问题无关。这只是一个错字,在原始项目中使用的是删除而不是添加,但问题仍然存在
  • 不完全是,因为您可以通过简单的滚动设置数百个侦听器。这意味着每个回调都试图在每个去抖动滴答声中触发数百次。
  • @JohnRuddell 好的,但这只有在我卸载并重新安装组件数百次时才会发生(这里不是这种情况),因为当前效果仅在安装/卸载时运行。
  • 是的,所以你应该记住手柄滚动。或者更好的是,不要放弃类语法,因为您已经有了一个可行的解决方案。

标签: javascript reactjs addeventlistener react-hooks


【解决方案1】:

在您的代码中,我看到了几个问题:

1) [] in useEffect 表示它不会看到任何状态变化,例如 goingUp 的变化。它总是会看到goingUp的初始值

2) debounce 不起作用。它返回一个新的去抖动函数。

3) 通常全局变量是一种反模式,认为它只适用于您的情况。

4) 如@skyboyer 所述,您的滚动监听器不是被动的。

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

const App = () => {
  const prevScrollY = useRef(0);

  const [goingUp, setGoingUp] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      if (prevScrollY.current < currentScrollY && goingUp) {
        setGoingUp(false);
      }
      if (prevScrollY.current > currentScrollY && !goingUp) {
        setGoingUp(true);
      }

      prevScrollY.current = currentScrollY;
      console.log(goingUp, currentScrollY);
    };

    window.addEventListener("scroll", handleScroll, { passive: true });

    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]);

  return (
    <div>
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
      <div style={{ background: "orange", height: 100, margin: 10 }} />
    </div>
  );
};

export default App;

https://codesandbox.io/s/react-setstate-from-event-listener-q7to8

【讨论】:

  • 很好的建议,太棒了,谢谢!只有一个问题——现在每当goingUp 发生变化时,都会调用useEffect(不是吗?)。这意味着每次调用它时,都会添加事件侦听器。这不会导致内存泄漏吗?
  • 每当goingUp改变时都会调用useEffect,只要用户不改变每秒滚动数百次的方向,性能就可以了。
  • 并且没有内存泄漏:每次更改goingUp都会调用removeEventListener
  • 我认为您还不必担心useEffect 的性能。 Codesandbox 上的代码显示它足够快。如果您仍然想进一步提高性能,您可以使用useDispatch 来记忆侦听器。 overreacted.io/a-complete-guide-to-useeffect 。但我认为这是代码的简单性和性能之间的糟糕交易
【解决方案2】:

总之,需要添加goingUp作为useEffect的依赖。

如果你使用[],你只会创建/删除一个带有函数的监听器(handleScroll,在初始渲染中创建)。换句话说,当重新渲染时,滚动事件侦听器仍然使用初始渲染中的旧handleScroll

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]);

使用自定义挂钩

我建议将整个逻辑移到自定义钩子中,这可以使您的代码更清晰且易于重用。我使用useRef 来存储之前的值。

export function useScrollDirection() {
  const prevScrollY = useRef(0)
  const [goingUp, setGoingUp] = useState(false);

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    if (prevScrollY.current < currentScrollY && goingUp) {
      setGoingUp(false);
    }
    if (prevScrollY.current > currentScrollY && !goingUp) {
      setGoingUp(true);
    }
    prevScrollY.current = currentScrollY;
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [goingUp]); 
  return goingUp ? 'up' : 'down';
}

【讨论】:

  • 我同意,这种方法看起来非常干净易读。谢谢你,很棒的见解。
  • 也许我不懂钩子,但是如果我想添加另一个自定义钩子,它也监听滚动事件,但在滚动到达时返回不同的数据,例如“顶部”或“底部”窗口底部的顶部。这两个钩子会互相干扰吗?
【解决方案3】:

您正在每次渲染上重新创建 handleScroll 函数,因此它指的是实际数据(goingup),因此您不需要在此处使用 useCallback

但是除了你重新创建 handleScroll 之外,你只被设置为事件侦听器的第一个实例 - 因为你给了 useEffect(..., []) 它只在挂载时运行一次。

一种解决方案是使用最新的handleScroll 重新订阅scroll。为此,您必须在 useEffect 中返回清理函数(现在它返回一个订阅)并删除 [] 以在每次渲染时运行它:

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
  return () => window.removeEventListener("scroll", handleScroll);
});

PS,你最好使用passive event listener 滚动。

【讨论】:

  • 从这个效果中移除依赖数组并没有帮助,而是在每次渲染时调用 add listener。即使我将handleScroll 移到函数之外以防止重新创建它,问题仍然存在。我需要做的就是以某种方式确定用户何时向上或向下滚动。
  • 它应该在每次渲染时调用addEventListener。否则,您需要更复杂的方法来使用实际数据(不是陈旧的数据)制作处理程序并使其去抖动。
【解决方案4】:

这是我对这种情况的解决方案

const [pageUp, setPageUp] = useState(false)
const lastScroll = useRef(0)

const checkScrollTop = () => {
  const currentScroll = window.pageYOffset
  setPageUp(lastScroll.current > currentScroll)
  lastScroll.current = currentScroll
}

// lodash throttle function
const throttledFunc = throttle(checkScrollTop, 400, { leading: true })

useEffect(() => {
  window.addEventListener("scroll", throttledFunc, false)
  return () => {
    window.removeEventListener("scroll", throttledFunc)
  }
}, [])

仅当componentDidMount 时添加事件监听器一次。不是pageUp 引起的所有变化。因此,我们不需要将pageUp 添加到useEffect 依赖项。

Hint: 你可以将throttledFunc 添加到useEffect 如果它发生变化并且你需要重新渲染组件

【讨论】:

    【解决方案5】:
    const objDiv = document.getElementById('chat_body');
    objDiv.scrollTop = objDiv.scrollHeight;
    

    【讨论】:

    • 能否请您注释您的代码或在代码示例上方添加解释以解释其工作原理,以便此答案普遍适用,而不仅仅是在 OP 的特定用例中?
    猜你喜欢
    • 2019-12-08
    • 1970-01-01
    • 2019-08-12
    • 2019-11-02
    • 1970-01-01
    • 2023-02-09
    • 2019-09-17
    • 2022-01-28
    • 2022-01-16
    相关资源
    最近更新 更多