【译】useEffect 的清除函数以及它的两种调用条件
![https://reacttraining.com/images/blog/useEffect-cleanup.jpg https://reacttraining.com/images/blog/useEffect-cleanup.jpg](https://reacttraining.com/images/blog/useEffect-cleanup.jpg)
本文翻译自:https://reacttraining.com/blog/useEffect-cleanup
React 的 effect
函数中会返回一个清除函数。在组件卸载时这个函数会被执行,这一点你可能已经了解了。
在我的讲习班上,我问开发者们这个函数什么时候会被调用,我通常会得到上面这一个答案。但是在我过去几年教过的 100 多个班上,我只遇到过一个人回答了完整答案(向他致敬)。
|
|
你可能在略读这篇文章,想直接知道它的第二种调用条件:
当 effect
的依赖项发生变化时,需要重新执行时,清除函数也会被调用。但是在下一个 effect 执行前,前一个 effect 的清除函数会先被调用。也许你需要读完整篇文章才能真正理解。。。
1 为什么需要清除函数
为了更好理解这两种调用条件,我们首先需要解释为什么你需要清除函数。
不幸的是,上面的代码中最尽人皆知的原因也是最容易误导人的,因此我们从这段代码开始。人们认为需要清除函数的理由是,如果不这样做,我们可能会“在一个已卸载的组件上进行 setState”。
在我们的另一篇文章中,已经深入解释过你不用去关注这个特定问题,但这个问题是一个好的切入点,我们需要修复这个问题,稍后会说明其他原因。
当组件卸载时,阻止 setState 的示例代码如下:
|
|
目前为止的重点是,我们返回了这个清除函数,但没有调用它,且上面的 promise 仍然处于 pending 状态。
现在,竟态条件(race condition)出现了。如果组件在 promise 解析之前就被卸载呢?(比如我们可能会跳转到其他页面),或者是 promise 先解析呢?
如果组件先卸载,上面这段代码会阻止我们在一个被卸载的组件上执行 setState – 如果这正是你想要的话。我说过其实这无关紧要。
确切地说,我确实需要上面这个清除函数的解决方案,但只是因为它修复了另外一个问题,由另一种竟态条件导致的问题。
2 竟态条件
让我们回到最初有 isMounted
的那段代码:
|
|
在 UserProfile
组件中,让我们假设可以点击这个 User 的朋友链接。当我们在浏览 users/1
时,我们可以快速点击跳转到 users/2
,然后 users/3
、users/4
,最后 users/5
。所有这些点击都会导致组件以一个新的 userId
属性重新渲染。
我们快速点击,因为 users/5
最后点击,他应该是最后看到的一个,但结果也许是这样的:
每次我们点击时,组件会重新渲染,effect 会接收一个新的 userId
,重新执行。当出现竟态条件时,网络返回的结果可能和请求的顺序不一致。最后看到的是网络最后返回的。我们想要看到 users/5
但可能 users/4
的请求最慢,最后被解析,你最后也会看到错误的 User 4。
当“切换” effects 时,清除函数也会被调用。换句话说,就是在依赖项变化,重新执行 useEffect()
函数前,React 会先执行上一个 effect 的清除函数。
从概念上讲,这就是 React 调用函数式组件的情况,让我们假设正在执行的 effect 为“current effect”:
|
|
假设接下来组件被重新渲染(userId
外的变化导致)。React 在再次用函数式组件,”current effect“ 仍属于第一次渲染:
|
|
你可以看到,有“recent render”,但是“current effect”来自于上一次渲染。
然后 userId
更新,组件再次重新渲染。这次重新渲染需要重新执行 useEffect()
。但在此之前,我们需要先“清除”旧的“current effect”。
|
|
如果我们像下面一样清除,就可以修复竟态:
|
|
看吧,和上面提到的为了避免在已卸载组件上 setState 的解决方案完全一致,除了变量命名更准确,毕竟我们现在知道清除函数并非只在组件卸载时被调用。
使用了这样的清除函数,每次 userId
更新时,我们都会先清除上一个 effect,这避免了当 promise 解析时,仍然执行 setState。然后执行下一个 userId
关联的 effect,同时将它变成“current effect”。
当点击很快时,我们更新“current effect”,之前的 promise 仍然处于 pending 态,最终的结果是我们通过阻止在之前的 effect 里执行 setState,来避免竟态。我们只想当 users/5
解析时看到 User 5,至于之前的无论是否解析,也不会因为调用了 setState 而导致最终结果出错。
有趣的时,这种方案也避免了在一个卸载的组件上执行 setState – 尽管这不是我们所关注的,但也很有趣。
在某种程度上,你可以将这两种条件抽象成一条规则:
当 effect 不再相关时,执行清除函数。当组件卸载,或者是我们需要抛弃一个旧的 effect 来创建一个新的时,都意味着不再相关。不管你怎么思考,只要能够理解,我都 OK。
祝编码愉快。