使用 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 的规则。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端e站

如果有所帮助,欢迎来杯奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值