Skip to content

useEffect

useEffect是React中最常用的Hook之一,它用于在函数组件中处理副作用。副作用是指那些不与组件渲染直接相关的操作,如网络请求、订阅、定时器、事件监听器等。useEffect的出现使得函数组件也能够处理副作用,大大简化了React组件的开发。

基本用法

引入useEffect

在使用useEffect之前,需要从React中引入它:

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

基本语法

useEffect是一个函数,它接受两个参数:

  1. 一个副作用函数,用于执行副作用操作
  2. 一个依赖数组,用于指定副作用函数依赖的变量
jsx
function Counter() {
  const [count, setCount] = useState(0);

  // 副作用函数
  useEffect(() => {
    // 执行副作用操作
    document.title = `You clicked ${count} times`;

    // 清理函数
    return () => {
      // 执行清理操作
      console.log('Cleanup');
    };
  }, [count]); // 依赖数组

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

副作用函数

副作用函数是useEffect的第一个参数,它用于执行副作用操作。副作用函数会在组件渲染后执行,并且在每次依赖项变化时重新执行。

清理函数

副作用函数可以返回一个清理函数,用于执行清理操作。清理函数会在组件卸载时执行,并且在每次副作用函数重新执行前执行。清理函数的作用是清理副作用函数创建的资源,如订阅、定时器、事件监听器等,避免内存泄漏。

依赖数组

依赖数组是useEffect的第二个参数,它是一个可选的数组,用于指定副作用函数依赖的变量。当依赖数组中的变量发生变化时,副作用函数会重新执行。

依赖数组的使用

空依赖数组

如果依赖数组为空,那么副作用函数只会在组件挂载时执行一次,清理函数只会在组件卸载时执行一次:

jsx
function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 只在组件挂载时执行一次
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => setData(data));

    return () => {
      // 只在组件卸载时执行一次
      console.log('Component unmounted');
    };
  }, []); // 空依赖数组

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

依赖项

如果依赖数组中包含变量,那么副作用函数会在组件挂载时执行一次,并且在每次依赖项变化时重新执行:

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 在组件挂载时执行一次,并且在userId变化时重新执行
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(user => setUser(user));

    return () => {
      // 在userId变化时执行一次,并且在组件卸载时执行一次
      console.log('Cleanup');
    };
  }, [userId]); // 依赖于userId

  return <div>{user ? user.name : 'Loading...'}</div>;
}

多个依赖项

如果依赖数组中包含多个变量,那么副作用函数会在组件挂载时执行一次,并且在任何一个依赖项变化时重新执行:

jsx
function SearchResults({ query, page }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 在组件挂载时执行一次,并且在query或page变化时重新执行
    fetch(`https://api.example.com/search?q=${query}&page=${page}`)
      .then(response => response.json())
      .then(results => setResults(results));
  }, [query, page]); // 依赖于query和page

  return (
    <ul>
      {results.map(result => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
}

没有依赖数组

如果没有提供依赖数组,那么副作用函数会在每次组件渲染后执行:

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

  useEffect(() => {
    // 在每次组件渲染后执行
    console.log('Component rendered');
  }); // 没有依赖数组

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

清理函数的使用

清理订阅

jsx
function SubscriptionExample() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 订阅数据
    const subscription = dataService.subscribe(data => setData(data));

    return () => {
      // 清理订阅
      subscription.unsubscribe();
    };
  }, []); // 空依赖数组

  return <div>{data}</div>;
}

清理定时器

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

  useEffect(() => {
    // 设置定时器
    const timer = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    return () => {
      // 清理定时器
      clearInterval(timer);
    };
  }, []); // 空依赖数组

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

清理事件监听器

jsx
function EventListenerExample() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    // 添加事件监听器
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    return () => {
      // 清理事件监听器
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组

  return <div>Window width: {width}</div>;
}

常见问题

无限循环

如果在副作用函数中更新了依赖数组中的变量,那么副作用函数会无限执行:

jsx
// 错误的做法:会导致无限循环
function InfiniteLoopExample() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 在副作用函数中更新了依赖数组中的变量
    setCount(count + 1);
  }, [count]); // 依赖于count

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

遗漏依赖项

如果副作用函数中使用了某个变量,但没有将其添加到依赖数组中,那么副作用函数可能会使用到过时的变量值:

jsx
// 错误的做法:遗漏了依赖项
function MissingDependencyExample() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => {
    // 使用了count变量,但没有将其添加到依赖数组中
    setMessage(`Count: ${count}`);
  }, []); // 空依赖数组

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

清理函数执行时机

清理函数会在组件卸载时执行,并且在每次副作用函数重新执行前执行:

jsx
function CleanupTimingExample({ userId }) {
  useEffect(() => {
    console.log(`Effect ran for userId: ${userId}`);

    return () => {
      console.log(`Cleanup ran for userId: ${userId}`);
    };
  }, [userId]); // 依赖于userId

  return <div>UserId: {userId}</div>;
}

function ParentComponent() {
  const [userId, setUserId] = useState(1);

  return (
    <div>
      <CleanupTimingExample userId={userId} />
      <button onClick={() => setUserId(userId + 1)}>Change User</button>
    </div>
  );
}

性能优化

使用useCallback

对于传递给子组件的回调函数,应该使用useCallback来缓存,避免不必要的副作用执行:

jsx
import React, { useState, useEffect, useCallback } from 'react';

function ChildComponent({ onDataChange }) {
  useEffect(() => {
    // 模拟数据变化
    const timer = setInterval(() => {
      onDataChange(Math.random());
    }, 1000);

    return () => clearInterval(timer);
  }, [onDataChange]); // 依赖于onDataChange

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

function ParentComponent() {
  const [data, setData] = useState(0);

  // 使用useCallback缓存回调函数
  const handleDataChange = useCallback((newData) => {
    setData(newData);
  }, []); // 空依赖数组

  return (
    <div>
      <p>Data: {data}</p>
      <ChildComponent onDataChange={handleDataChange} />
    </div>
  );
}

使用useMemo

对于计算密集型的操作,应该使用useMemo来缓存计算结果,避免不必要的计算:

jsx
import React, { useState, useEffect, useMemo } from 'react';

function ExpensiveComponent({ items }) {
  // 使用useMemo缓存计算结果
  const expensiveValue = useMemo(() => {
    console.log('Calculating expensive value');
    let result = 0;
    for (let i = 0; i < items.length; i++) {
      for (let j = 0; j < 1000000; j++) {
        result += i + j;
      }
    }
    return result;
  }, [items]); // 依赖于items

  useEffect(() => {
    // 使用计算结果
    console.log('Expensive value changed:', expensiveValue);
  }, [expensiveValue]); // 依赖于expensiveValue

  return <div>Expensive Value: {expensiveValue}</div>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([1, 2, 3]);

  return (
    <div>
      <ExpensiveComponent items={items} />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setItems([...items, items.length + 1])}>Add Item</button>
    </div>
  );
}

优化网络请求

对于网络请求,应该使用防抖或节流来优化:

jsx
import React, { useState, useEffect, useCallback } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // 使用防抖优化搜索
  const debouncedSearch = useCallback(
    debounce((searchQuery) => {
      fetch(`https://api.example.com/search?q=${searchQuery}`)
        .then(response => response.json())
        .then(data => setResults(data));
    }, 300),
    []
  );

  useEffect(() => {
    if (query) {
      debouncedSearch(query);
    } else {
      setResults([]);
    }
  }, [query, debouncedSearch]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

// 防抖函数
function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

高级用法

多个useEffect

在一个组件中,可以使用多个useEffect来处理不同的副作用:

jsx
function MultipleEffectsExample() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);

  // 处理定时器
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖数组

  // 处理网络请求
  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => setData(data));
  }, []); // 空依赖数组

  return (
    <div>
      <p>Count: {count}</p>
      <p>Data: {data ? JSON.stringify(data) : 'Loading...'}</p>
    </div>
  );
}

与useReducer结合使用

对于复杂的状态管理,可以使用useReduceruseEffect结合使用:

jsx
import React, { useReducer, useEffect } from 'react';

// 定义reducer
function reducer(state, action) {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_DATA':
      return { ...state, data: action.payload, loading: false };
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
}

function DataFetcher({ url }) {
  // 使用useReducer管理状态
  const [state, dispatch] = useReducer(reducer, {
    data: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    // 重置状态
    dispatch({ type: 'SET_LOADING', payload: true });

    // 发送网络请求
    fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => dispatch({ type: 'SET_DATA', payload: data }))
      .catch(error => dispatch({ type: 'SET_ERROR', payload: error.message }));
  }, [url]); // 依赖于url

  const { data, loading, error } = state;

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return <div>{JSON.stringify(data)}</div>;
}

自定义Hook

可以将useEffect封装到自定义Hook中,复用副作用逻辑:

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

// 自定义Hook:用于获取数据
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    fetch(url)
      .then(response => response.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(error => {
        if (isMounted) {
          setError(error);
          setLoading(false);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

// 使用自定义Hook
function DataFetcher() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{JSON.stringify(data)}</div>;
}

面试常见问题

1. 什么是useEffect?它的作用是什么?

useEffect是React中的一个Hook,它用于在函数组件中处理副作用。副作用是指那些不与组件渲染直接相关的操作,如网络请求、订阅、定时器、事件监听器等。useEffect的作用是:

  • 允许函数组件处理副作用
  • 替代类组件中的生命周期方法(componentDidMount、componentDidUpdate、componentWillUnmount)
  • 提供清理机制,避免内存泄漏

2. useEffect的依赖数组有什么作用?

useEffect的依赖数组用于指定副作用函数依赖的变量。当依赖数组中的变量发生变化时,副作用函数会重新执行。依赖数组的作用是:

  • 控制副作用函数的执行时机
  • 避免不必要的副作用执行
  • 避免无限循环

3. 如果依赖数组为空,useEffect的行为是什么?

如果依赖数组为空,那么副作用函数只会在组件挂载时执行一次,清理函数只会在组件卸载时执行一次。这相当于类组件中的componentDidMountcomponentWillUnmount的组合。

4. 如何在useEffect中处理异步操作?

useEffect中处理异步操作的方法是:

  • 在副作用函数中定义异步函数并调用
  • 使用async/await语法
  • 注意清理操作,避免内存泄漏
jsx
useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      setData(data);
    } catch (error) {
      setError(error);
    }
  };

  fetchData();
}, []);

5. 如何避免useEffect中的无限循环?

避免useEffect中的无限循环的方法是:

  • 确保副作用函数中更新的变量不在依赖数组中
  • 使用函数式更新,避免依赖于前一个状态
  • 对于复杂的状态管理,使用useReducer

6. 清理函数的作用是什么?什么时候执行?

清理函数的作用是清理副作用函数创建的资源,如订阅、定时器、事件监听器等,避免内存泄漏。清理函数会在以下情况下执行:

  • 组件卸载时
  • 副作用函数重新执行前

7. useEffect和useLayoutEffect的区别是什么?

useEffectuseLayoutEffect的区别是:

  • 执行时机useEffect在DOM更新后执行,useLayoutEffect在DOM更新前执行
  • 阻塞渲染useLayoutEffect会阻塞渲染,useEffect不会阻塞渲染
  • 适用场景useLayoutEffect适用于需要在DOM更新前读取或修改DOM的场景,如滚动位置调整;useEffect适用于不需要阻塞渲染的场景,如网络请求、订阅等

8. 如何在useEffect中使用setState?

useEffect中使用setState的方法是:

  • 确保setState的依赖项正确设置
  • 对于依赖于前一个状态的更新,使用函数式更新
  • 避免在副作用函数中直接更新依赖数组中的变量,这会导致无限循环
jsx
useEffect(() => {
  // 正确的做法:使用函数式更新
  setCount(prevCount => prevCount + 1);
}, []); // 空依赖数组

9. 如何优化useEffect的性能?

优化useEffect的性能的方法是:

  • 使用依赖数组,避免不必要的副作用执行
  • 使用useCallback缓存回调函数
  • 使用useMemo缓存计算结果
  • 对于网络请求,使用防抖或节流
  • 拆分多个useEffect,处理不同的副作用

10. 什么是副作用的清理?为什么需要清理?

副作用的清理是指在组件卸载或副作用函数重新执行前,清理副作用函数创建的资源,如订阅、定时器、事件监听器等。需要清理的原因是:

  • 避免内存泄漏:如果不清理资源,会导致内存使用量不断增加
  • 避免不必要的操作:如果不清理资源,会导致组件已经卸载后,仍然执行副作用操作
  • 避免错误:如果不清理资源,会导致在组件已经卸载后,仍然尝试更新组件的状态

总结

useEffect是React中最常用的Hook之一,它用于在函数组件中处理副作用。副作用是指那些不与组件渲染直接相关的操作,如网络请求、订阅、定时器、事件监听器等。

useEffect的基本用法是:接受一个副作用函数和一个依赖数组作为参数。副作用函数会在组件渲染后执行,并且在每次依赖项变化时重新执行。副作用函数可以返回一个清理函数,用于清理副作用函数创建的资源。

在使用useEffect时,需要注意:

  • 正确设置依赖数组,避免不必要的副作用执行
  • 避免在副作用函数中直接更新依赖数组中的变量,这会导致无限循环
  • 正确使用清理函数,避免内存泄漏
  • 对于复杂的副作用逻辑,考虑使用自定义Hook或useReducer

通过系统学习useEffect的使用方法和最佳实践,你将能够更好地处理React组件的副作用,构建高质量的React应用。

好好学习,天天向上