MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React Hooks中的依赖数组详解

2022-06-187.7k 阅读

什么是 React Hooks 中的依赖数组

在 React Hooks 的世界里,useEffectuseMemouseCallback 等钩子函数都有一个重要的概念——依赖数组。依赖数组本质上是一个数组,它包含了一些变量,这些变量的变化会触发钩子函数的特定行为。

useEffect 为例,useEffect 用于在函数组件中执行副作用操作,比如数据获取、订阅或手动修改 DOM。其基本语法如下:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 副作用操作
    console.log('副作用执行');
    return () => {
      // 清理函数,在组件卸载或依赖变化时执行
      console.log('清理副作用');
    };
  }, []); // 依赖数组

  return <div>My Component</div>;
}

在上述代码中,第二个参数 [] 就是依赖数组。如果依赖数组为空,useEffect 中的副作用操作只会在组件挂载后执行一次,并且在组件卸载时执行清理函数。

依赖数组的作用

  1. 控制副作用执行时机:通过依赖数组,我们可以精确控制 useEffect 副作用的执行时机。当依赖数组中的任何一个值发生变化时,useEffect 会重新执行。例如:
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log(`Count 变化为: ${count}`);
    return () => {
      console.log('清理副作用');
    };
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

在这个 Counter 组件中,useEffect 的依赖数组为 [count]。每当 count 的值发生变化(通过点击按钮调用 setCount),useEffect 中的副作用代码就会重新执行,打印出 Count 变化为: [新的 count 值]。并且在组件卸载时,会执行清理函数打印 清理副作用

  1. 性能优化:对于 useMemouseCallback 钩子,依赖数组起到性能优化的作用。useMemo 用于缓存计算结果,只有当依赖数组中的值发生变化时才会重新计算。useCallback 用于缓存函数,同样只有依赖数组中的值变化时才会重新创建函数。
import React, { useMemo, useState } from 'react';

function ExpensiveCalculation({ a, b }) {
  const calculate = () => {
    // 模拟一个复杂的计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += a * b;
    }
    return result;
  };

  const memoizedResult = useMemo(() => calculate(), [a, b]);

  return <p>计算结果: {memoizedResult}</p>;
}

function App() {
  const [num1, setNum1] = useState(1);
  const [num2, setNum2] = useState(2);

  return (
    <div>
      <ExpensiveCalculation a={num1} b={num2} />
      <input
        type="number"
        value={num1}
        onChange={(e) => setNum1(parseInt(e.target.value, 10))}
      />
      <input
        type="number"
        value={num2}
        onChange={(e) => setNum2(parseInt(e.target.value, 10))}
      />
    </div>
  );
}

ExpensiveCalculation 组件中,useMemo 缓存了 calculate 函数的计算结果。只有当 ab 的值发生变化时,才会重新执行 calculate 函数,避免了不必要的复杂计算,提升了性能。

依赖数组的常见问题与解决方法

  1. 遗漏依赖导致的问题:如果在 useEffect 中使用了某个变量,但没有将其添加到依赖数组中,可能会出现逻辑错误。例如:
import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [id, setId] = useState(1);
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://example.com/api/data/${id}`);
      const result = await response.json();
      setData(result);
    };
    fetchData();
  }, []); // 遗漏了 id 作为依赖

  return (
    <div>
      <input
        type="number"
        value={id}
        onChange={(e) => setId(parseInt(e.target.value, 10))}
      />
      {data && <p>{JSON.stringify(data)}</p>}
    </div>
  );
}

在上述代码中,useEffect 依赖数组为空,但在数据获取逻辑中使用了 id。这意味着即使 id 发生变化,useEffect 也不会重新执行,导致数据无法更新。解决方法是将 id 添加到依赖数组中:

useEffect(() => {
  const fetchData = async () => {
    const response = await fetch(`https://example.com/api/data/${id}`);
    const result = await response.json();
    setData(result);
  };
  fetchData();
}, [id]);

这样,当 id 变化时,useEffect 会重新执行,从而获取最新的数据。

  1. 无限循环问题:如果不小心将状态更新函数添加到依赖数组中,可能会导致无限循环。例如:
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    setCount(count + 1);
  }, [setCount]); // 错误地将 setCount 放入依赖数组

  return <p>Count: {count}</p>;
}

在这个例子中,useEffect 的依赖数组包含 setCount。每次 useEffect 执行时,setCount 会触发状态更新,进而导致 useEffect 再次执行,形成无限循环。解决方法是确保依赖数组只包含真正影响副作用逻辑的变量,而不是状态更新函数:

useEffect(() => {
  setCount(count + 1);
}, []); // 正确的依赖数组,只执行一次

或者在有条件的情况下更新状态,避免无限循环:

useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

复杂场景下的依赖数组处理

  1. 多个依赖且部分依赖触发不同逻辑:有时候,useEffect 可能依赖多个变量,但不同变量的变化需要执行不同的逻辑。例如:
import React, { useState, useEffect } from 'react';

function ComplexEffect() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [data, setData] = useState(null);

  useEffect(() => {
    if (name) {
      // 根据 name 进行数据获取
      const fetchByName = async () => {
        const response = await fetch(`https://example.com/api/name/${name}`);
        const result = await response.json();
        setData(result);
      };
      fetchByName();
    }
  }, [name]);

  useEffect(() => {
    if (age) {
      // 根据 age 进行数据获取
      const fetchByAge = async () => {
        const response = await fetch(`https://example.com/api/age/${age}`);
        const result = await response.json();
        setData(result);
      };
      fetchByAge();
    }
  }, [age]);

  return (
    <div>
      <input
        type="text"
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="number"
        placeholder="Age"
        value={age}
        onChange={(e) => setAge(parseInt(e.target.value, 10))}
      />
      {data && <p>{JSON.stringify(data)}</p>}
    </div>
  );
}

在这个 ComplexEffect 组件中,使用了两个 useEffect,分别依赖 nameage。这样可以在不同变量变化时执行不同的数据获取逻辑。

  1. 依赖数组包含对象或数组:当依赖数组包含对象或数组时,需要特别小心。因为对象和数组是引用类型,即使其内部值没有变化,只要引用地址改变,就会触发钩子函数重新执行。例如:
import React, { useState, useEffect } from 'react';

function ObjectDependency() {
  const [user, setUser] = useState({ name: 'John', age: 30 });

  useEffect(() => {
    console.log('User 变化');
  }, [user]);

  const updateUser = () => {
    // 这里虽然只改变了 age,但对象引用地址改变
    setUser({ ...user, age: user.age + 1 });
  };

  return (
    <div>
      <p>{JSON.stringify(user)}</p>
      <button onClick={updateUser}>Update User</button>
    </div>
  );
}

在上述代码中,每次点击 Update User 按钮,user 对象的引用地址都会改变,导致 useEffect 重新执行。如果只想在 user 对象内部某些属性真正变化时才触发 useEffect,可以使用深度比较。一种方法是使用 lodash 库中的 isEqual 函数:

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

function ObjectDependency() {
  const [user, setUser] = useState({ name: 'John', age: 30 });

  useEffect(() => {
    console.log('User 变化');
  }, [user]);

  const updateUser = () => {
    // 这里虽然只改变了 age,但对象引用地址改变
    setUser({ ...user, age: user.age + 1 });
  };

  return (
    <div>
      <p>{JSON.stringify(user)}</p>
      <button onClick={updateUser}>Update User</button>
    </div>
  );
}

通过 usePrevious 自定义钩子和 isEqual 函数,只有当 user 对象的实际内容发生变化时,useEffect 才会重新执行。

自定义 Hooks 中的依赖数组

在自定义 Hooks 中,依赖数组的使用规则与普通 Hooks 类似。例如,我们创建一个自定义 Hooks 来处理数据获取:

import { useState, useEffect } from'react';

function useDataFetching(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

function MyComponent() {
  const { data, loading, error } = useDataFetching('https://example.com/api/data');

  if (loading) {
    return <p>Loading...</p>;
  }
  if (error) {
    return <p>Error: {error.message}</p>;
  }
  return (
    <div>
      {data && <p>{JSON.stringify(data)}</p>}
    </div>
  );
}

useDataFetching 自定义 Hooks 中,useEffect 的依赖数组为 [url]。当 url 变化时,会重新执行数据获取逻辑。这样,在不同的组件中使用 useDataFetching 时,只要传入不同的 url,就能根据 url 的变化获取不同的数据。

总结依赖数组的最佳实践

  1. 明确依赖:仔细分析副作用逻辑中使用的所有外部变量,并将它们添加到依赖数组中。这样可以确保副作用在相关变量变化时正确执行。
  2. 避免不必要的依赖:不要将不会影响副作用逻辑的变量添加到依赖数组中,以免导致不必要的重新执行,影响性能。
  3. 处理引用类型依赖:对于对象和数组等引用类型的依赖,要注意它们的引用地址变化可能导致的意外触发。可以使用深度比较等方法来解决这个问题。
  4. 拆分复杂逻辑:如果副作用逻辑依赖多个变量且不同变量触发不同行为,考虑使用多个 useEffect 分别处理,使代码逻辑更加清晰。

通过正确理解和使用 React Hooks 中的依赖数组,我们能够更好地控制组件的副作用行为,提升应用的性能和稳定性。在实际开发中,不断积累经验,遵循最佳实践,能够更加高效地使用 React Hooks 构建复杂的前端应用。