为啥每次渲染都会调用 `useEffect` 的清理功能?

Posted

技术标签:

【中文标题】为啥每次渲染都会调用 `useEffect` 的清理功能?【英文标题】:Why is the cleanup function from `useEffect` called on every render?为什么每次渲染都会调用 `useEffect` 的清理功能? 【发布时间】:2019-11-23 04:08:05 【问题描述】:

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

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

我尝试设置一个空数组,但我拥有的 setInterval 只会运行一次 有一篇很长的关于用 hooks 处理 setInterval 的帖子,这不是一个普通的任务:) 这可能对你有帮助 overreacted.io/making-setinterval-declarative-with-react-hooks【参考方案2】:

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

【讨论】:

您好,感谢您的回复。这很清楚。但是,如果我没有将 array 设置为依赖项,即我将其保留为空数组,则 setInterval 将只运行一次。你知道如何解决这个问题吗? 这取决于您希望它何时运行?也许setTimeout 是你要找的?【参考方案3】:

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

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

提示

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

【讨论】:

【参考方案4】:

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

我不确定你在哪里读到这个,但这个陈述是不正确的。 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,并且您正在以设定的时间间隔更改数组。每次更改数组时,钩子都会重新运行。

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

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

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

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

【讨论】:

引用“React 在组件卸载时执行清理”。直接来自 React:reactjs.org/docs/hooks-effect.html 继续阅读,您会发现不仅在卸载组件时。这就是我要说的。 必须对此投反对票,因为您引用的陈述没有错,只是不完整。它也来自官方文档,因此暗示他们读错了没有帮助。 来自 React:“React 在组件卸载时执行清理。但是,正如我们之前所了解的,效果会在每次渲染时运行,而不仅仅是一次。这就是为什么 React 还会清理之前的效果在下次运行效果之前渲染。”【参考方案5】:

正如其他人所说,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);
     ;
  , []);

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

【讨论】:

以上是关于为啥每次渲染都会调用 `useEffect` 的清理功能?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 useEffect 钩子在第一次渲染时不调用函数?

如何避免在每次渲染时触发useEffect?

为啥在组件加载时不调用 useEffect(()=...,[]) ?

每次更改页面时都会调用 React useEffect (with []) (React Router)

为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?

在 useEffect() 中未调用函数