您的位置:

深入了解useEffect钩子函数

useEffect钩子函数是React提供的一个用于处理副作用的函数,它在组件函数运行完成后立即执行。React生命周期钩子函数的升级版本,可以完成类似componentDidMount、componentDidUpdate等多种功能。本文将从多个方面对useEffect做详细阐述,帮助读者更加深入了解这个重要的组件钩子。

一、useState和useEffect的关系

useStateuseEffect这两个钩子函数被称为React的核心功能。它们的用途和相关操作是相互依存的,因为在大部分情况下我们的业务操作都涉及到这两个组件钩子的使用。在使用useState的情况下,我们会使用setState方法来操控组件的状态,这时候如果需要改变状态,需要重新渲染组件,就需要使用useEffect来处理需要重新渲染的情况。useState与useEffect的配合使用,是实现React状态响应式编程的核心关键。

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

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = \`当前计数值为:\${count}\`;
  }, [count]);

  return (
    
   

计数值为:{count}

) } export default Counter; `}

在上述代码中,我们定义了一个计数器组件,包含一个useState和一个useEffect。使用useState定义状态,通过useEffect实现副作用,副作用内容是修改浏览器标签的title值。每次useState的setCount方法被调用,useEffect都会重新渲染组件,使得计数器得到更新,同时组件DOM也随即更新。

二、useEffect用法详解

在前面的例子中,我们已经有所了解useEffect的主要作用——渲染组件的同时完成一些副作用的操作。除此之外,还有一些需要注意的细节点:

1、useEffect可以有一个回调函数和一个依赖项参数

在使用useEffect时,可以传递一个回调函数,以及一个数组。当数组中的某一个元素发生变化时,才会触发回调函数的执行。这一特性是非常重要的,可以帮助我们优化需要操作的过程。

{`
useEffect(() => {
  // 这里是回调函数
}, [依赖项1, 依赖项2, ...]);
`}

2、useEffect回调函数可以返回另一个函数

useEffect也可以有一个返回值,通常是另一个函数。返回的函数会在useEffect的下一次运行发布之前运行。具体应用比如清除计时器、移除订阅等操作。

{`
useEffect(() => {
  const timer = setInterval(() => {
    console.log('这是一条定时器消息');
  }, 1000);
  // 返回一个函数来清除定时器
  return () => clearInterval(timer);
}, []);
`}

3、useEffect可以给定一个空的数组作为参数

当我们给useEffect传递一个空数组作为参数时,它就相当于是componentDidMount,只会运行一次,它会在组件加载后只运行一次。这很方便,因为您不需要再在函数的依赖项中列出任何东西。

{`
useEffect(() => {
  // 这是回调函数
}, []);
`}

4、useEffect回调函数可以是异步函数

如果我们使用async/await关键字将useEffect回调函数定义为异步函数,那么下面的组件代码就会渲染一次,然后出现警告:

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

const InvalidUseEffect = () => {
  const [count, setCount] = useState(0);

  useEffect(async () => {
    console.log(count);
  }, [count]);

  return (
    
   

计数:{count}

) } export default InvalidUseEffect; `}

这个警告的原因是,异步函数没有返回清除副作用的函数。解决这个问题的方法是将回调函数定义为一个立即调用的异步函数:

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

const ValidUseEffect = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    (async function () {
      console.log(count);
    })()
  }, [count]);

  return (
    
   

计数:{count}

) } export default ValidUseEffect; `}

三、常见问题及解决方案

1、多次调用useEffect的问题

在某些情况下,我们会多次调用useEffect,导致副作用函数被多次执行。解决这个问题的最简单方法是把需要移除的清除函数作为操作功能返回:

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

  useEffect(() => {
    function handleStatusChange(status) {
      console.log(status);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  }, [props.friend.id]);

  // ...
`}

2、用useEffect执行异步操作时需要使用取消机制

使用useEffect钩子执行异步操作时,最好使用取消机制,以便组件随时都能取消异步操作。因为我们手动清除的副作用就是在组件卸载的时候自动清除副作用,而在setState之后,对应的副作用并不会马上生效。这里有一个useEffect的示例:

{`
function GitHubUser({ login }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(\`https://api.github.com/users/\${login}\`)
      .then(res => res.json())
      .then(setData)
      .catch(console.error);
  }, [login]);

  if (data) {
    return (
      
   

{data.login}

); } return null; } `}

可以看到,在上面代码中,每次使用fetch进行数据请求都有可能存在网络错误,发生卡顿效果,用户体验上也不够友好。为了解决这个问题,我们可以使用AbortController来取消异步操作,从而保证正常的请求流程不会因此被中断。

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

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;

    fetch(url, { signal })
      .then(res => res.json())
      .then(response => {
        setData(response);
        setLoading(false);
      })
      .catch(e => {
        console.error(e);
      });

    return function cleanup() {
      abortController.abort();
   };
  }, [url]);

  return { data, loading };
}

export default useFetch;
`}

3、在useEffect中调用setState可能导致过度渲染

以MutationObserver为例,许多人会根据一个状态变量的值,将其他状态属性设置为一个新值以进行DOM操作。这种操作是很常见的,但不幸的是,这种模式下的组件很容易陷入重新渲染的陷阱中。在这个示例中,重绘会调用setIsObserving(true),这又会触发useEffect的执行,然后过度进行重新渲染。

{`
function DivObserver({ targetNode }) {
  const [isObserving, setIsObserving] = useState(false);
  const [showObserver, setShowObserver] = useState(false);

  useEffect(() => {
    if (isObserving) {
      setShowObserver(true);
    } else {
      setShowObserver(false);
    }
    const observer = new MutationObserver(() => {
      setIsObserving(true);
    });
    observer.observe(targetNode.current, { attributes: true });
    return () => observer.disconnect();
  }, [isObserving, targetNode]);

  return (
    <>
      {showObserver ? '观察器' : '未开启观察器'}
      
    
  );
}
`}

为了优化并避免过度渲染,我们可以使用另外一个useState来解决问题:

{`
function DivObserver({ targetNode }) {
  const [weShouldObserve, setWeShouldObserve] = useState(false);
  const [showObserver, setShowObserver] = useState(false);
  const [numMutations, setNumMutations] = useState(0);

  useEffect(() => {
    if (weShouldObserve) {
      setShowObserver(true);
      const observer = new MutationObserver(() => {
        setNumMutations(n => n + 1);
      });
      observer.observe(targetNode.current, { attributes: true });
      return () => observer.disconnect();
    }
    setShowObserver(false);
  }, [weShouldObserve, targetNode]);

  return (
    <>
      {showObserver ? '观察器' : '未开启观察器'}
      ({numMutations} mutations)
      
    
  );
}
`}

现在,我们有了一个额外的状态变量weShouldObserve,它是渲染组件所必需的。我们在每一个useEffect里只观察weShouldObserve,而不是isObserving。如果isObserving变化,那么仍会触发所有副作用调用,并重新渲染组件。而weShouldObserve变化,它只会重新触发这一个useEffect,而不会触发其他useEffect的不必要重新渲染。