为什么每次渲染都会调用 useEffect 的清理函数?

Why is the cleanup function from `useEffect` called on every render?

我一直在学习 React,我读到 useEffect 返回的函数是用来清理的,React 在组件卸载时执行清理。

所以我对它进行了一些试验,但在下面的示例中发现每次组件重新渲染时都会调用该函数,而不是仅在它从 DOM 卸载时调用,即它 console.log("unmount"); 每次组件重新渲染。

这是为什么?

function Something({ setShow }) {
  const [array, setArray] = useState([]);
  const myRef = useRef(null);

  useEffect(() => {
    const id = setInterval(() => {
      setArray(array.concat("hello"));
    }, 3000);
    myRef.current = id;
    return () => {
      console.log("unmount");
      clearInterval(myRef.current);
    };
  }, [array]);

  const unmount = () => {
    setShow(false);
  };

  return (
    <div>
      {array.map((item, index) => {
        return (
          <p key={index}>
            {Array(index + 1)
              .fill(item)
              .join("")}
          </p>
        );
      })}
      <button onClick={() => unmount()}>close</button>
    </div>
  );
}

function App() {
  const [show, setShow] = useState(true);

  return show ? <Something setShow={setShow} /> : null;
}

实例:https://codesandbox.io/s/vigilant-leavitt-z1jd2

看代码我猜是因为第二个参数[array]。您正在更新它,所以它会调用 re-render。尝试设置一个空数组。

每次状态更新都会调用 re-render 并卸载,并且该数组正在更改。

React 文档对此有一个 explanation section

简而言之,原因是因为这样的设计可以防止过时的数据和更新错误。

React 中的 useEffect 挂钩旨在处理初始渲染和任何后续渲染 (here's more about it)。


效果是通过它们的依赖关系来控制的,而不是通过使用它们的组件的生命周期来控制的。

任何时候效果的依赖关系发生变化,useEffect 将清除以前的效果并 运行 新效果。

这样的设计更有预见性 - each render has its own independent (pure) behavioral effect。这确保 UI 始终显示正确的数据(因为 React 的心智模型中的 UI 是特定渲染状态的屏幕截图)。

我们控制效果的方式是通过它们的依赖性。

为了防止在每次渲染时 运行 进行清理,我们只需要不更改效果的依赖项即可。

具体来说,在您的情况下,正在进行清理,因为 array 正在发生变化,即 Object.is(oldArray, newArray) === false

useEffect(() => {
  // ...
}, [array]);
//  ^^^^^ you're changing the dependency of the effect

您正在使用以下行引起此更改:

useEffect(() => {
  const id = setInterval(() => {
    setArray(array.concat("hello")); // <-- changing the array changes the effect dep
  }, 3000);
  myRef.current = id;

  return () => {
    clearInterval(myRef.current);
  };
}, [array]); // <-- the array is the effect dep

好像在意料之中。根据此处的文档,useEffect 在首次渲染、每次更新和卸载后调用。

https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Tip

If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate and before componentWillUnmount combined.

React performs the cleanup when the component unmounts.

我不确定你是在哪里读到这篇文章的,但这种说法是不正确的。 React performs the cleanup when the dependencies to that hook changes and the effect hook needs to run again with new values。此行为是为了保持视图对更改数据的反应性。离开官方的例子,假设一个应用程序从朋友的个人资料订阅状态更新。作为好朋友,您决定与他们解除朋友关系并与其他人成为朋友。现在应用程序需要取消订阅以前朋友的状态更新并收听新朋友的更新。 useEffect 的工作方式很自然且容易实现。

 useEffect(() => { 
    chatAPI.subscribe(props.friend.id);

    return () => chatAPI.unsubscribe(props.friend.id);
  }, [ props.friend.id ])

通过将好友id包含在依赖列表中,我们可以表明只有当好友id发生变化时才需要运行钩子。

在您的示例中,您在依赖项列表中指定了 array,并且您正在以设定的时间间隔更改数组。每次更改数组时,钩子 re运行s.

您只需从依赖项列表中删除数组并使用 setState 挂钩的回调版本即可实现正确的功能。回调版本总是对上一版本的状态进行操作,所以不需要每次数组变化时都去刷新hook

  useEffect(() => {
    const id = setInterval(() => setArray(array => [ ...array, "hello" ]), 3000);

    return () => {
      console.log("unmount");
      clearInterval(id);
    };
  }, []);

一些额外的反馈是直接在 clearInterval 中使用 id,因为在创建清理函数时该值已关闭(捕获)。无需将其保存到 ref.

正如其他人所说,useEffect 取决于在 useEffect 的第二个参数中指定的“数组”的变化。因此,将其设置为空数组,有助于在组件安装时触发一次 useEffect。

这里的技巧是改变数组的先前状态。

setArray((arr) => arr.concat("hello"));

见下文:

  useEffect(() => {
     const id = setInterval(() => {
         setArray((arr) => arr.concat("hello"));
     }, 3000);
     myRef.current = id;
     return () => {
        console.log("unmount");
        clearInterval(myRef.current);
     };
  }, []);

我 fork 你的 CodeSandbox 来演示: https://codesandbox.io/s/heuristic-maxwell-gcuf7?file=/src/index.js