[React Hooks 翻译] 4-8 Effect Hook

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码
  • 什么是副作用:数据获取,设置订阅以及手动更改React组件中的DOM都是副作用
  • 可以将useEffect Hook视为componentDidMount,componentDidUpdate和componentWillUnmount的组合。

不清理的副作用

有时,我们希望在React更新DOM之后运行一些额外的操作。如:

  1. 网络请求
  2. 手动修改DOM
  3. 日志记录

这些操作不需要清理,也就是说可以运行它们并立即忘记它们。下面我们分别看看class和Hook是如何处理的

使用Class

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
复制代码

注意,我们在componentDidMount和componentDidUpdate的时候执行了同样的代码。下面看看Hooks怎么处理的

使用Hook

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码
  • useEffect做了什么?

    • useEffect告诉React组件需要在渲染后执行某些操作,React将记住useEffect传递的函数(也就是副作用函数),并在执行DOM更新后调用。
    • 本例中我们设置了文档标题,我们也可以执行获取数据或调用其他API
  • 为什么要在组件内部调用useEffect?

    • 可以直接访问state
    • 不需要特殊的API来读取state,state已经在函数作用域内了。
  • useEffect每次render后都执行吗?

    • 是的
    • 默认情况下,它在第一次渲染之后和每次更新之后运行。 (我们稍后将讨论如何自定义它。)
    • 比起“mount”和"update",可能考虑"render"之后执行某些操作更容易。React确保是在DOM更新之后执行副作用

细节解释

现在我们对effect有了一定的了解,下面的代码应该很容易懂了

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
复制代码

也许你会发现每次render传给useEffect的函数都不同,这是有意为之的。实际上,这就是为什么我们即使在useEffect内部读取state也不用担心state过期。每次re-render,我们都会安排一个不同的effect去取代之前的那个effect。在某种程度上,这使得effect更像是render的结果的一部分——每个effect“属于”特定的render。

需要清理的副作用

使用Class

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}
复制代码

注意componentDidMount和componentWillUnmount的代码需“相互镜像”。生命周期方法迫使我们拆分相互关联的逻辑

使用Hook

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
复制代码
  • 为什从副作用函数返回一个函数?
    • 这是副作用的清理机制,是可选的。
    • 每个副作用都可以返回一个在它之后清理的函数。这样添加和删除逻辑就可以放在一起了。它们实际上是同一个副作用的一部分。
  • React什么时候会清理副作用?
    • 组件卸载时
    • **执行下一次副作用之前。**副作用在每次render的时候都会执行,React在下次运行副作用之前还清除前一次render的副作用。我们将在后面讨论为什么这么做,以及发生性能问题之后如何跳过清除行为

Note

副作用函数不一定要返回具名函数。

使用Effect须知

使用多个Effect进行关注点分离

之前在使用Hooks的动机那一章就有提到,使用Hook的原因之一是class的生命周期使不相干的逻辑混在一起,相关的逻辑散在各处,比如下面的代码,

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
复制代码

使用Hooks如何解决这个问题呢?

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}
复制代码
  • Hooks让我们根据逻辑拆分代码,而不是根据生命周期方法名称来拆分代码。
  • React将按照指定的顺序组件使用的每个effect。

解释:为什么每次更新都要运行effect?

如果你习惯使用class组件,你可能会很疑惑为什么不是组件卸载的时候执行清理副作用的工作,而是在每次re-render的时候都要执行。下面我们就看看为什么

前面我们介绍了一个示例FriendStatus组件,该组件显示朋友是否在线。我们的类从this.props读取friend.id,在组件挂载之后订阅朋友状态,并在卸载前取消订阅。

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
复制代码

**但是如果在该组件还在显示的状态下,friend属性改变了怎么办?**组件显示的将是原来那个friend的在线状态。这是一个bug。然后后面取消订阅调用又会使用错误的friend ID,还会在卸载时导致内存泄漏或崩溃。

在class组件中,我们需要添加componentDidUpdate来处理这种情况

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
复制代码

忘记正确处理componentDidUpdate常常导致bug。

现在考虑使用Hooks实现这个组件

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
复制代码

这下没bug了,即使我们什么也没改

默认情况下useEffect会在应用下一个effect之前清除之前的effect。为了解释清楚,请看下面这个订阅和取消订阅的调用序列

// Mount with { friend: { id: 100 } } props
// Run first effect
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     

// Update with { friend: { id: 200 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); 
// Run next effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     

// Update with { friend: { id: 300 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); 
// Run next effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     

// Unmount
// Clean up last effect
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); 
复制代码

默认情况下这种做法确保了逻辑连贯性,并且防止了常常在class组件里出现的因为忘写update逻辑而导致的bug

通过跳过effect优化性能

某些情况下,在每次渲染后清理或执行effect可能会产生性能问题。在class组件中,我们可以通过在componentDidUpdate中编写与prevProps或prevState的比较来解决这个问题

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
复制代码

此要求很常见,它已内置到useEffect Hook API中。

如果重新渲染之间某些值没有改变,你可以告诉React跳过执行effect。只需要将一个数组作为可选的第二个参数传递给useEffect

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
复制代码

在上面的例子中,我们将[count]作为第二个参数传递。

这是什么意思?

  • 如果count是5,然后组件重新渲染之后count还是5,React就会比较前一次渲染的5和下一次渲染的5。因为数组中的所有项都是相同的(5 === 5),所以React会跳过执行effect。这就是我们的优化
  • 当渲染后count更新到了6,React就会比较前一次渲染的5和下一次渲染的6。这一次 5 !== 6,所以React会重新执行effect。

如果数组中有多个项,即使其中一个项不同,React也会重新执行这个effect。

对具有清理工作的effect同样适用

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
复制代码

将来,第二个参数可能会被构建时转换自动添加。

Note

  • 如果你适用了这个优化,请确保数组包含了组件作用域内(例如props和state)effect用到的、随时间变化的所有值。否则代码可能引用了上一次render的那个过期的变量。Learn more about how to deal with functions and what to do when the array changes too often
  • 如果仅执行effect并清理一次(在mount和unmount上),可以传递一个空数组([])作为第二个参数。
    • 这告诉React你的效果不依赖于来自props或state,所以它永远不需要重新运行。这不作为一种特殊情况处理 - 它直接遵循依赖项数组的工作方式。
    • 如果传递一个空数组([]),effect中的props和state将始终具有其初始值。
    • 虽然传递[]作为第二个参数更接近componentDidMount和componentWillUnmount,但是有更好的解决方案来避免经常重新自己执行effect( better solutions
  • 不要忘记React延迟执行行useEffect直到浏览器绘制完成,所以做额外的工作并不是什么问题。
  • 我们推荐使用 exhaustive-deps 规则(这是 eslint-plugin-react-hooks 的一部分)。它会在错误地指定依赖项时发出警告并建议修复。

下一篇

下面我们将了解钩子规则 - 它们对于使钩子工作至关重要。

转载于:https://juejin.im/post/5cea49c5518825685e02c0dd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值