React函数组件使用Effect Hook(副作用钩子)

使用Effect Hook(副作用钩子)

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount(挂载完成),componentDidUpdate(更新完成) 和 componentWillUnmount(即将销毁前) 这三个函数的组合。

Did : 做了… Will: 将要…

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

在 React 组件中有两种常见副作用操作:**需要清除的 **和 不需要清除的。我们来更仔细地看一下他们之间的区别。

无需清除的 effect

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

使用 class 的示例

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMountcomponentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
	
  // DOM挂载了
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  // DOM更新了
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  
  render() {
    return (
      

You clicked {this.state.count} times

); } }

注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

使用 Hook 的示例

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  
  useEffect(() => { // 每次渲染组件DOM后执行此回调函数(即,挂载完成和更新完成生命周期)
    document.title = `You clicked ${count} times`;
  });
  return (
    

You clicked {count} times

); }

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffectuseEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

不同之处

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的 effect

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

使用 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';
  }
}

使用 Hook 的示例

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

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

  useEffect(() => { // 每次渲染组件DOM后执行此回调函数(即,挂载完成和更新完成生命周期)
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 每次重新渲染前都会执行(class的 componentWillUnmount 只在卸载组件执行)
    return function cleanup() { // 函数名非必需,可使用箭头函数
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

使用 Effect 的提示

使用Hook的目的

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

提示: 使用多个 Effect 实现关注点分离

可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:

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);
    };
  });
  // ...
}

解释: 为什么每次更新的时候都要运行 Effect

如果你已经习惯了使用 class,那么你或许会疑惑为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。

忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源

提示: 通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

对于有清除操作的 effect 同样适用:

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

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

未来版本,可能会在构建时自动添加第二个参数。

注意:

  1. 如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

  2. 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。

  3. 如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMountcomponentWillUnmount 思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

  4. 我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

参考文献:Effect Hook

你可能感兴趣的:(React,react.js,javascript,前端)