使用 Effect Hook

Posted 前端e站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 Effect Hook相关的知识,希望对你有一定的参考价值。

Effect Hook可以让你在function组件里搞出点副作用出来。

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都属于effect的范畴。无论你是否把它们称为effect,你其实以前都在组件里用到过它们了。

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

React组件里有两种常见的effect,一种需要清理,另一种不需要。

不需要清理的effect

有时我们需要在React更新DOM后执行额外的代码。网络请求,手工DOM修改,日志等都是常见的不需要清理的effect。之所以如此说是因为我们运行完之后可以立刻把它们忘了。让我们比较一下class和Hooks是怎么实现这种effect的。

使用class的例子

在React的class组件里,render方法自身不会引起effect。在这个里面还早了些——我们特别希望是React更新DOM了之后再执行我们的effect代码。

这就是为什么我们把effect代码放在了class的componentDidMount和componentDidUpdate里。回到例子里,这里有一个class组件,当React更新完DOM之后会去修改文档标题。

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>
    );
  

注意到我们不得不在class里的两个生命周期方法里重复代码。

这是因为在许多场景下,不管是第一次组件加载还是组件更新,我们需要执行相同的effect代码。从概念上来说,我们希望是发生在每次渲染之后——但React类组件没有像这样的回调方法。尽管可以把代码抽到一个独立的方法里,但我们仍然要在class里调用两次。

使用Hooks的例子

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用来干什么?使用这个Hook,相当于你告诉React你的组件需要在渲染之后执行一些操作。React将会把你传递给它的方法记住,并在DOM更新之后执行。在这个示例中,我们设置了文档标题,但其实也可以拉取数据或者调用其他必要的API。

为什么在组件内部调用useEffect?把useEffect放在一个组件内部让我们可以使用count这个state变量(或者属性)。我们不需要一个特定的API来读取它——它已经在方法作用域里了。Hooks拥抱了javascript的闭包,避免引入新的React的API。

是不是在每次渲染后都执行useEffect?是的!默认情况下,在第一次渲染和每一次更新后都会被运行到。不需要思考当前是加载还是更新,你只要把它看作在每一次渲染之后都会执行。React保证执行前DOM操作都已经更新完成了。

详细说明

现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:

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

我们声明了count这个state变量,然后告诉React我们需要使用一个effect方法。我们把一个方法传递给useEffect这个Hook。这个方法就是我们的effect。在effect内部,我们用document.title浏览器API修改了文档标题。我们可以在effect里读取count变量。当React渲染组件时,它会记住我们的effect,并在更新完DOM之后运行它。这意味着effect在每次渲染后都会被调用,包括第一次。

有经验的JavaScript开发者会发现我们传给useEffect的方法在每次渲染后不是同一个实例。这其实是有意为之的。实际上,这样做可以让我们在effect里读取count值且不用担心数据老化。每次重新渲染,都会有一个不同的effect来替代前一个。这样子使得effect更像是渲染结果的一部分——每一个effect都属于一次特定的渲染。

提示

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

需要清理的effect

之前我们看了不需要清理的effect。但是还是有一些场景是需要的。比如我们可能想建立一个数据源的订阅。这时候清理就十分必要了,不然会引起内存泄漏。比较一下class组件和Hooks的写法。

使用class的例子

在React的class里,我们一般在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里干的是相反的。生命周期回调迫使我们必须把相同意义逻辑代码分离在不同的地方。

使用Hooks的例子

我们看看用Hooks如何写这个例子。

你可能觉得我们需要把effect分开才能执行清理动作。但其实用useEffect可以把添加订阅和清理订阅绑在一起。如果你的effect返回一个方法,React会在清理的时候来执行它。

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';

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect?React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为。

注意

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

回顾

我们已经知道了组件渲染后可以用useEffect来执行effect。一些effect可以返回用以清理的方法。

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

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => 
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    ;
);

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

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

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

如果你对 Effect Hook 的机制已经有很好的把握,或者暂时难以消化更多内容,你现在稍后学习 Hook 的规则。

以上是关于使用 Effect Hook的主要内容,如果未能解决你的问题,请参考以下文章

React常用hook的优化useEffect浅比较

如何在 Effect Hook 中使用 axios?

前端学习(3294):effect hook

前端学习(3293):effect hook

[React] Use the React Effect Hook in Function Components

React的Effect Hook解决函数组件的性能问题和潜在bug!