React 16.7.0-alpha hooks 之 effect

转载于免费视频网站:www.rails365.net/articles/re…html

Effect Hook可使得你在函数组件中执行一些带有反作用的方法。react

import { 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>
  );
}
复制代码

上面这段代码是基于上个state hook计数器的例子,可是如今添加了新的功能: 咱们将文档标题设置为自定义消息,包括点击次数。git

数据获取,设置订阅以及手动更改React组件中的DOM都是反作用的示例。不管你是否习惯于将这些操做称为“反作用”(或仅仅是“效果”),但你以前可能已经在组件中执行了这些操做。github

提示: 若是你熟悉React类生命周期方法,则能够将useEffect Hook视为componentDidMountcomponentDidUpdatecomponentWillUnmount的组合。数组

React组件中有两种常见的反作用:那些不须要清理的反作用,以及那些须要清理的反作用。让咱们更详细地看一下这种区别。浏览器

无需清理的反作用

有时,咱们但愿在 React更新DOM以后运行一些额外的代码。 网络请求,手动改变DOM和日志记录是不须要清理的效果(反作用,简称'效果')的常见示例。咱们这样说是由于咱们能够运行它们并当即忘记它们。让咱们比较一下classhooks如何让咱们表达这样的反作用。网络

使用class的例子

React类组件中,render方法自己不该该致使反作用。这太早了 - 咱们一般但愿在React更新DOM以后执行咱们的效果。闭包

这就是为何在React类中,咱们将反作用放入componentDidMountcomponentDidUpdate中。回到咱们的示例,这里是一个React计数器类组件,它在ReactDOM进行更改后当即更新文档标题:函数

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>
    );
  }
}
复制代码

请注意咱们如何在类中复制这两个生命周期方法之间的代码。布局

这是由于在许多状况下,咱们但愿执行相同的反作用,不管组件是刚安装仍是已更新。从概念上讲,咱们但愿它在每次渲染以后发生 - 可是React类组件没有这样的方法(render方法应该避免反作用)。咱们能够提取一个单独的方法,但咱们仍然须要在两个地方调用它。

如今让咱们看看咱们如何使用useEffect Hook作一样的事情。

使用Hooks的例子

import { 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有什么做用? 经过使用这个Hook,你告诉React你的组件须要在渲染后执行某些操做。React将记住你传递的函数(咱们将其称为“效果”),并在执行DOM更新后稍后调用它。在这个效果中,咱们设置文档标题,但咱们也能够执行数据提取或调用其余命令式API

为何在组件内调用useEffect 在组件中使用useEffect让咱们能够直接从效果中访问状态变量(如count或任何道具)。咱们不须要特殊的API来读取它 - 它已经在函数范围内了。Hooks拥抱JavaScript闭包,并避免在JavaScript已经提供解决方案的状况下引入特定于ReactAPI

每次渲染后useEffect都会运行吗? 是的。默认状况下,它在第一次渲染以后和每次更新以后运行。 (咱们稍后会讨论如何自定义它。)你可能会发现更容易认为效果发生在“渲染以后”,而不是考虑“挂载”和“更新”。React保证DOM在运行‘效果’时已更新。

详细说明

如今咱们对这个hook更加的了解了,那让咱们再看看下面的例子:

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

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

咱们声明了count状态变量,而后告诉React咱们须要使用效果。咱们将一个函数传递给useEffect Hook,这个函数就是效果(反作用)。在咱们的效果中,咱们使用document.title浏览器API设置文档标题。咱们能够读取效果中的最新count,由于它在咱们的函数范围内。当React渲染咱们的组件时,它会记住咱们使用的效果,而后在更新DOM后运行咱们的效果。每次渲染都会发生这种状况,包括第一次渲染。

有经验的JavaScript开发人员可能会注意到,传递给useEffect的函数在每次渲染时都会有所不一样。这是有意的。事实上,这就是让咱们从效果中读取计数值而不用担忧它没有改变的缘由。每次咱们从新渲染时,咱们都会安排一个不一样的效果,取代以前的效果。在某种程度上,这使得效果更像是渲染结果的一部分 - 每一个效果“属于”特定渲染。咱们将在本页后面更清楚地看到为何这有用。

注意:componentDidMountcomponentDidUpdate不一样,使用useEffect的效果不会阻止浏览器更新屏幕。这使应用感受更具响应性。大多数效果不须要同步发生。在他们这样作的不常见状况下(例如测量布局),有一个单独的useLayoutEffect Hook,其APIuseEffect相同。

须要清理的反作用

以前,咱们研究了如何表达不须要任何清理的反作用。可是,有些效果须要清理。例如,咱们可能但愿设置对某些外部数据源的订阅。在这种状况下,清理是很是重要的,这样咱们就不会引入内存泄漏!让咱们比较一下咱们如何使用类和Hooks来实现它。

使用class的例子

React类中,一般会在componentDidMount中设置订阅,并在componentWillUnmount中清除它。例如,假设咱们有一个ChatAPI模块,可让咱们订阅朋友的在线状态。如下是咱们如何使用类订阅和显示该状态:

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';
  }
}
复制代码

请注意componentDidMountcomponentWillUnmount如何相互做用。生命周期方法迫使咱们拆分这个逻辑,即便它们中的概念代码都与相同的效果有关。

注意: 眼尖的你可能会注意到这个例子还须要一个componentDidUpdate方法才能彻底正确。咱们暂时忽略这一点,但会在本页的后面部分再回过头来讨论它。

使用hooks的例子

你可能认为咱们须要单独的效果来执行清理。可是添加和删除订阅的代码是如此紧密相关,以致于useEffect旨在将它保持在一块儿。若是你的效果返回一个函数,React将在清理时运行它:

import { useState, useEffect } from 'react';

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() : {
    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何时清理效果? 当组件卸载时,React执行清理。可是,正如咱们以前所了解的那样,效果会针对每一个渲染运行而不只仅是一次。这就是React在下次运行效果以前还清除前一渲染效果的缘由。咱们将讨论为何这有助于避免错误以及如何在之后发生性能问题时选择退出此行为

注意 咱们没必要从效果中返回命名函数。咱们在这里只是为了说明才加的命名,但你能够返回箭头函数。

归纳

咱们已经了解到useEffect让咱们在组件渲染后表达不一样类型的反作用。某些效果可能须要清理,所以它们返回一个函数:

useEffect(() : {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () : {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});
复制代码

其余效果可能没有清理阶段,也不会返回任何内容。好比:

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

若是你以为你对Effect Hook的工做方式有了很好的把握,或者你感到不知所措,那么如今就能够跳转到关于Hooks规则。

使用效果的提示

咱们将继续深刻了解使用React用户可能会产生好奇心的useEffect的某些方面。

提示:使用多重效果分离问题

这是一个组合了前面示例中的计数器和朋友状态指示器逻辑的组件:

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
    });
  }
  // ...
复制代码

请注意设置document.title的逻辑如何在componentDidMountcomponentDidUpdate之间拆分。订阅逻辑也在componentDidMountcomponentWillUnmount之间传播。componentDidMount包含两个任务的代码。

那么,Hooks如何解决这个问题呢?就像你能够屡次使用状态挂钩同样,你也可使用多种效果。这让咱们将不相关的逻辑分红不一样的效果:

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

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() : {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () : {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}
复制代码

Hooks容许咱们根据它正在作的事情而不是生命周期方法名称来拆分代码。 React将按照指定的顺序应用组件使用的每一个效果。

说明:为何效果在每一个更新上运行

若是你习惯了类,你可能想知道为何每次从新渲染后效果的清理阶段都会发生,而不是在卸载过程当中只发生一次。让咱们看一个实际的例子,看看为何这个设计能够帮助咱们建立更少bug的组件。

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

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

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

可是若是friend prop在组件出如今屏幕上时发生了变化,会发生什么? 咱们的组件将继续显示不一样朋友的在线状态。这是一个错误。卸载时咱们还会致使内存泄漏或崩溃,由于取消订阅会使用错误的朋友ID。

在类组件中,咱们须要添加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
    );
  }
复制代码

忘记正确处理componentDidUpdateReact应用程序中常见的bug漏洞。

如今考虑使用Hooks的这个组件的版本:

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

它不会受到这个bug的影响。 (但咱们也没有对它作任何改动。)

没有用于处理更新的特殊代码,由于默认状况下useEffect会处理它们。它会在应用下一个效果以前清除以前的效果。为了说明这一点,这里是一个订阅和取消订阅调用的序列,该组件能够随着时间的推移产生:

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

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

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

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

此行为默认确保一致性,并防止因为缺乏更新逻辑而致使类组件中常见的错误。

提示:经过跳过效果优化性能

在某些状况下,在每次渲染后清理或应用效果可能会产生性能问题。在类组件中,咱们能够经过在componentDidUpdate中编写与prevPropsprevState的额外比较来解决这个问题:

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

这个要求很常见,它被内置到useEffect Hook API中。若是在从新渲染之间没有更改某些值,则能够告诉React跳过应用效果。为此,将数组做为可选的第二个参数传递给useEffect

useEffect(() : {
  document.title = `You clicked ${count} times`;
}, [count]); // 当count改变的时候回再次运行这个效果
复制代码

在上面的例子中,咱们传递[count]做为第二个参数。这是什么意思?若是count为5,而后咱们的组件从新渲染,count仍然等于5,则React将比较前一个渲染的[5]和下一个渲染的[5]。由于数组中的全部项都是相同的(5 === 5),因此React会跳过这个效果。这是咱们的优化。

当咱们使用count更新为6渲染时,React会将前一渲染中[5]数组中的项目与下一渲染中[6]数组中的项目进行比较。此次,React将从新运行效果,由于5!== 6若是数组中有多个项目,React将从新运行效果,即便其中只有一个不一样。

这也适用于具备清理阶段的效果:

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

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

注意 若是使用此优化,请确保该数组包含外部做用域中随时间变化且效果使用的任何值,换句话说就是要在这个效果函数里有意义。 不然,代码将引用先前渲染中的旧值。咱们还将讨论Hooks API参考中的其余优化选项。

若是要运行效果并仅将其清理一次(在装载和卸载时),则能够将空数组([])做为第二个参数传递。 这告诉React你的效果不依赖于来自propsstate的任何值,因此它永远不须要从新运行。这不做为特殊状况处理 - 它直接遵循输入数组的工做方式。虽然传递[]更接近熟悉的componentDidMountcomponentWillUnmount生命周期,但咱们建议不要将它做为一种习惯,由于它常常会致使错误,除非你明确你本身在作什么, 如上所述。 不要忘记React推迟运行useEffect直到浏览器绘制完成后,因此作额外的工做不是问题。

相关文章
相关标签/搜索