[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以后运行一些额外的操做。如:html

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

这些操做不须要清理,也就是说能够运行它们并当即忘记它们。下面咱们分别看看class和Hook是如何处理的react

使用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怎么处理的git

使用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作了什么?github

    • useEffect告诉React组件须要在渲染后执行某些操做,React将记住useEffect传递的函数(也就是反作用函数),并在执行DOM更新后调用。
    • 本例中咱们设置了文档标题,咱们也能够执行获取数据或调用其余API
  • 为何要在组件内部调用useEffect?npm

    • 能够直接访问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 的一部分)。它会在错误地指定依赖项时发出警告并建议修复。

下一篇

下面咱们将了解钩子规则 - 它们对于使钩子工做相当重要。

相关文章
相关标签/搜索