47

我一直在学习 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

4

5 回答 5

55

React 在组件卸载时执行清理。

我不确定你在哪里读到这个,但这个陈述是不正确的。当对该钩子的依赖发生变化并且效果钩子需要使用新值再次运行时,React 会执行清理。此行为是有意保持视图对更改数据的反应性。离开官方的例子,假设一个应用程序从朋友的个人资料中订阅状态更新。作为你的好朋友,你决定与他们解除朋友关系并与其他人成为朋友。现在该应用程序需要取消订阅以前朋友的状态更新并收听新朋友的更新。这是很自然的,并且很容易通过useEffect工作方式实现。

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

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

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

在您的示例中,您已array在依赖项列表中指定,并且您正在以设定的时间间隔更改数组。每次更改数组时,钩子都会重新运行。

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

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

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

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

于 2019-07-16T13:44:16.657 回答
3

React 文档对此有一个解释部分

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

React 中的useEffect钩子旨在处理初始渲染和任何后续渲染(这里有更多关于它的信息)。


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

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

这样的设计更具可预测性——每个渲染都有自己独立的(纯)行为效果。这确保了 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
于 2019-07-13T21:55:38.287 回答
0

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

这里的诀窍是改变 Array 的先前状态。

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);
     };
  }, []);

我分叉了您的 CodeSandbox 进行演示: https ://codesandbox.io/s/heuristic-maxwell-gcuf7?file=/src/index.js

于 2020-10-08T10:00:56.573 回答
0

查看代码我可以猜到它是因为第二个参数[array]。您正在更新它,因此它将调用重新渲染。尝试设置一个空数组。

每次状态更新都会调用重新渲染和卸载,并且该数组正在更改。

于 2019-07-13T21:53:54.517 回答
0

这似乎是意料之中的。根据此处的文档, useEffect在第一次渲染、每次更新和卸载后调用。

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

小费

如果你熟悉 React 类的生命周期方法,你可以将 useEffect Hook 想象成 componentDidMount、componentDidUpdate 和之前的 componentWillUnmount 组合。

于 2019-07-13T21:59:25.167 回答