无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

使用 class 的示例

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。
这就是为什么在 React class 中,我们把副作用操作放到 componentDidMountcomponentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

  1. class Example extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. };
  7. }
  8. componentDidMount() {
  9. document.title = `You clicked ${this.state.count} times`;
  10. }
  11. componentDidUpdate() {
  12. document.title = `You clicked ${this.state.count} times`;
  13. }
  14. render() {
  15. return (
  16. <div>
  17. <p>You clicked {this.state.count} times</p>
  18. <button onClick={() => this.setState({ count: this.state.count + 1 })}>
  19. Click me
  20. </button>
  21. </div>
  22. );
  23. }
  24. }

注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
现在让我们来看看如何使用 useEffect 执行相同的操作。

使用 Hook 的示例

我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:

  1. import React, { useState, useEffect } from 'react';
  2. function Example() {
  3. const [count, setCount] = useState(0);
  4. useEffect(() => {
  5. document.title = `You clicked ${count} times`;
  6. });
  7. return (
  8. <div>
  9. <p>You clicked {count} times</p>
  10. <button onClick={() => setCount(count + 1)}>
  11. Click me
  12. </button>
  13. </div>
  14. );
  15. }

默认情况下,它在第一次渲染之后每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后(state状态变更都会导致渲染)”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。


需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源,需要在组件卸载时清除订阅。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

使用 Hook 的示例

如何使用 Hook 编写这个组件。
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

  1. import React, { useState, useEffect } from 'react';
  2. function FriendStatus(props) {
  3. const [isOnline, setIsOnline] = useState(null);
  4. useEffect(() => {
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  9. // Specify how to clean up after this effect:
  10. return function cleanup() {
  11. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  12. };
  13. });
  14. if (isOnline === null) {
  15. return 'Loading...';
  16. }
  17. return isOnline ? 'Online' : 'Offline';
  18. }

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。

小结

了解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:

  1. useEffect(() => {
  2. function handleStatusChange(status) {
  3. setIsOnline(status.isOnline);
  4. }
  5. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  6. return () => {
  7. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  8. };
  9. });

其他的 effect 可能不必清除,所以不需要返回。

  1. useEffect(() => {
  2. document.title = `You clicked ${count} times`;
  3. });

effect Hook 使用同一个 API 来满足这两种情况。

参考

https://react.docschina.org/docs/hooks-effect.html