【问题标题】:When does the useEffect's callback's return statement execute?useEffect 的回调的 return 语句什么时候执行?
【发布时间】:2021-03-21 07:39:47
【问题描述】:

我想澄清一下我对这里发生的事情的理解。任何能提高我目前理解的细节都将不胜感激。

function Timer() {

    let [time, setTime] = useState(5);

    useEffect(() => {
        let timer = setInterval(() => {
          setTime(time - 1);
        }, 1000)
        return () => clearInterval(timer);
    }, );

    return <div>{time}</div>
}

export default Timer

https://codesandbox.io/s/cranky-chaplygin-g1r0p

  1. time 正在初始化为 5
  2. useEffect 已读取。它的回调必须准备好稍后触发。
  3. div 被渲染。
  4. useEffect 的回调被执行。 setInterval 的回调准备触发。当然useEffectreturn 语句不会在这里触发,因为如果触发它会取消计时器(并且计时器确实有效)。
  5. 大约 1 秒后,setInterval 的回调触发将 time 的状态更改为 4。
  6. 现在一个状态已经改变,函数被重新执行。 time,一个新变量,被初始化为新的时间状态。
  7. 读取了一个新的useEffect,它的回调准备好稍后触发。 (这是因为useEffect() 没有第二个参数)。
  8. 组件函数的return 语句被执行。这有效地重新渲染了div
  9. 在某些时候,前一个useEffectreturn 语句会执行(这会禁用前一个useEffect 中的timer)。我不确定何时会发生这种情况。
  10. “新”useEffect 的回调被执行。

【问题讨论】:

  • useEffect 有一个依赖数组,如果你不使用它,useEffect 将在每次状态更改时被调用。对于像这样的计时器,将[] 传递给你的依赖将停止计时器不断地被创造/毁灭。 PS:盲目地将[] 传递给所有 useEffects 并不总是一个好主意,请确保还包括任何应该影响您的渲染的依赖项,
  • 谢谢@Keith,但这不是我要问的问题。我已经剥离了原始代码,并希望确保我对“生命周期”中发生的事情的理解是正确的。如果您对我的理解有任何 cmets,那会很有帮助
  • No.8,重新渲染是什么意思,只有在Props或State发生变化时才会重新渲染。你得到一个重新渲染,因为你改变了状态,仅此而已。
  • 澄清一下,在本例中,您在每次渲染时创建一个间隔,请确保您了解此计数器为何有效,尽管这是一个非常糟糕的示例,但而不是你如何在 React 中创建计数器。
  • @tonitone120 在您的示例中使用SetTimeout 而不是SetInterval 会更好,或者如果您确实使用SetInterval 使用依赖项[],也可以使这更完整-证明,像计时器等异步的东西应该做一个安装检查,否则你最终可能会在一个未安装的组件上调用setState

标签: javascript reactjs react-hooks


【解决方案1】:

功能组件在其状态更改后是否会“卸载”,这是否会导致先前的 useEffect 回调返回语句执行?

不,组件在其生命周期结束时仅卸载一次,React 允许通过提供带有 return 语句的空 dep 数组来使用 useEffect 钩子执行回调:

useEffect(() => {
  return () => {
    console.log("unmounts");
  };
}, []);

组件何时卸载?

当它的父级停止渲染它时。见conditional rendering


您能否帮助我理解,一般而言或在我的问题中的示例中,useEffect 的 return 语句何时执行?

取决于 dep 数组:

  • 如果[] 为空,则卸载。
  • 如果它有依赖关系[value1,value2],则依赖关系发生变化(浅比较)。
  • 如果它没有依赖项(useEffect 没有第二个参数),它会在每次渲染时运行。

查看后续问题useEffect in depth / use of useEffect?

【讨论】:

  • 谢谢@DennisVash。我已经更新了我的问题,以显示我对代码中发生的事情的理解,因为我对useEffect 有了更好的理解。我不确定我的 useEffect 的 return 语句何时执行。你说它在每次渲染时都“运行”。初始执​​行/渲染是否包含在其中?当然,这个想法的问题在于,useEffectreturn 会在定时器的回调有机会执行之前立即取消定时器(并且定时器确实工作)。
【解决方案2】:

您对事件顺序的理解是正确的。唯一缺少的是效果回调和清理的精确时间。

当组件重新渲染时,任何useEffects 都将分析其依赖数组以进行更改。如果有更改,则该效果回调将运行。这些回调保证按照它们在组件中声明的顺序运行。例如,在下面,a 将始终记录在 b 之前。

const App = () => {
    const [num, setNum] = React.useState(0);
    React.useEffect(() => {
      setInterval(() => {
        setNum(num => num + 1);
      }, 1000);
    }, []);
    React.useEffect(() => {
      console.log('a', num);
    }, [num]);
    React.useEffect(() => {
      console.log('b', num);
    }, [num]);
    return num;
}

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

这些效果回调将运行shortly after the browser re-paints

现在将效果清理回调添加到组合中。这些将始终同步运行就在渲染的效果回调运行之前。例如,假设组件从 Render A 开始,在 Render A 中,一个效果挂钩返回了一个清理回调。然后,一些状态发生变化,并发生到 Render B 的转换,并且存在一个 useEffect 和一个包含状态变化的依赖数组。会发生什么:

  • 功能组件将使用新的道具/状态调用,用于渲染 B
  • 组件在函数末尾返回新标记
  • 如有必要,浏览器会重新绘制屏幕
  • 渲染 A 中的 清理功能 将运行
  • 将运行来自渲染 B 的 效果回调

你可以看到最后两个动作的源代码here

commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);

第一次调用会调用之前渲染的所有清理回调。第二个调用调用当前渲染的所有效果回调。当前渲染效果回调在执行之前的渲染清理回调之后同步运行。

【讨论】:

  • 所以RenderA的清理回调总是在RenderB的useEffects的常规回调之前执行?即使useEffect1 包含在renderB 上看到的常规回调,在源顺序上比useEffect2 更高(在renderB 上,它只有清理回调要执行),useEffect2 的清理回调将在useEffect1 的常规回调之前执行?
  • 此外,在清理回调中,它们按源顺序执行?
  • 是的,它就是这样工作的。先前渲染运行的所有清理(按效果声明顺序),然​​后是当前渲染运行的所有效果回调(按效果声明顺序)。
  • 我也明白(虽然承认,只是一般地),当处理(功能)组件及其子组件时,子组件的useEffect(常规)回调将执行在父母之前。当涉及到“清理”回调时,我可以期待类似的行为吗?例如。子组件的清理回调将在其父组件的清理回调之前执行。这可能不是有用的信息(请随时证明我错了),但我想我也可以理解。
【解决方案3】:

你说得差不多了,让我试着更清楚地说明这一点

在深入研究之前,我们需要了解,如果useEffect 没有任何内容作为第二个参数(正如问题中的那样),则传递给useEffect 的函数将在每次渲染中执行。

  1. 传递给useState() 的参数用作初始值。因此时间被初始化为 5
  2. useEffect 已执行 => setTimeout() 现在将在 1 秒后执行 => useEffect 的返回值让我们将其称为 func1 以供稍后执行
  3. 时间的值,现在是 5,被渲染了
  4. 1 秒后setTimeout() 执行并将时间值更改为 4 并设置它
  5. 因此会发生重新渲染并再次执行useEffect。此时useEffect会执行func1来清理之前的效果,然后传递给useEffect的函数会被执行,所以会初始化一个新的setTimeout()并返回语句,让我们调用这个func2以供稍后执行
  6. 时间的值,现在是 4,被渲染了
  7. 1 秒后setTimeout() 执行并将时间值更改为 3 并设置它
  8. 现在回到第 4 点,这个过程继续无限地进行

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-29
    • 2014-05-04
    • 1970-01-01
    • 2021-09-10
    • 2022-12-09
    • 1970-01-01
    • 2022-01-07
    • 1970-01-01
    相关资源
    最近更新 更多