React 19 生命周期:从入门到实战的完整指南

今天来聊聊 React 19 中的生命周期。如果你之前写过类组件,可能还记得那一堆 componentDidMountcomponentWillUnmount 之类的方法。不过现在都 2025 年了,函数组件 + Hooks 早就成为主流,所以今天我们主要聊聊在函数组件中如何优雅地处理生命周期。

先说说什么是生命周期

简单来说,生命周期就是组件从"出生"到"死亡"的整个过程。就像我们人一样,有出生、成长、衰老、死亡,React 组件也是:

复制代码
创建 → 挂载到页面 → 更新数据 → 再更新 → 最终卸载

在这个过程中的每个阶段,我们可能需要做一些事情,比如:

  • 组件刚挂载时去请求数据
  • 数据更新后重新计算一些值
  • 组件要卸载时清理定时器、取消订阅等

React 19 的生命周期:Hooks 时代

在现代 React 中,我们主要用这几个 Hook 来处理生命周期:

1. useEffect - 最常用的生命周期 Hook

这是你会用得最多的一个。我刚开始学的时候,总把它理解成"副作用",听起来挺高大上,其实说白了就是:在组件渲染后做点事情

基础用法:挂载时执行

tsx 复制代码
import { useEffect, useState } from 'react';

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

  // 组件挂载后执行
  useEffect(() => {
    console.log('组件挂载了!');
    
    // 获取用户数据
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, []); // 空数组表示只在挂载时执行一次

  if (loading) return <div>加载中...</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

这里有个坑 :很多新手会忘记写依赖数组 [],结果每次组件重新渲染都会触发 useEffect,然后就陷入无限循环了。我当年也踩过这个坑,debug 了半天才发现 😅

监听变化:依赖数组

tsx 复制代码
function SearchResults({ keyword }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 只有当 keyword 变化时才执行
    console.log(`搜索关键词变了: ${keyword}`);
    
    if (keyword.trim()) {
      fetch(`/api/search?q=${keyword}`)
        .then(res => res.json())
        .then(data => setResults(data));
    } else {
      setResults([]);
    }
  }, [keyword]); // 依赖 keyword

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

实战经验:依赖数组里要诚实,用了什么就写什么。别想着偷懒省略,ESLint 会提醒你的(虽然我知道你可能会想关掉提示 😂)。

清理函数:组件卸载时执行

这个特别重要!不做清理会导致内存泄漏,然后你的应用就会越跑越慢。

tsx 复制代码
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    console.log('定时器开始');
    
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // 返回清理函数
    return () => {
      console.log('定时器清理');
      clearInterval(timer); // 一定要清理!
    };
  }, []);

  return <div>已运行 {seconds} 秒</div>;
}

我踩过的坑:曾经写了个聊天应用,WebSocket 连接没有在组件卸载时关闭,结果用户切换页面后,后台还在疯狂接收消息,最后浏览器直接卡死了。所以清理函数真的很重要!

2. useLayoutEffect - DOM 更新后立即执行

这个跟 useEffect 很像,但执行时机不同:

  • useEffect:浏览器绘制完成后执行(异步)
  • useLayoutEffect:DOM 更新后、浏览器绘制前执行(同步)

听起来有点绕?我画个图:

复制代码
状态更新 → DOM 更新 → useLayoutEffect 执行 → 浏览器绘制 → useEffect 执行

什么时候用 useLayoutEffect?

说实话,99% 的情况用 useEffect 就够了。只有在这些场景才需要 useLayoutEffect

场景 1:需要测量 DOM 尺寸

tsx 复制代码
import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip({ children }) {
  const tooltipRef = useRef(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    // 立即测量 DOM,避免闪烁
    const rect = tooltipRef.current.getBoundingClientRect();
    
    setPosition({
      top: rect.bottom + 10,
      left: rect.left
    });
  }, [children]);

  return (
    <>
      <div ref={tooltipRef}>{children}</div>
      <div 
        className="tooltip"
        style={{ 
          position: 'fixed',
          top: position.top,
          left: position.left 
        }}
      >
        提示信息
      </div>
    </>
  );
}

如果用 useEffect,用户会看到提示框先出现在错误位置,然后闪一下跳到正确位置,体验很差。用 useLayoutEffect 就能在绘制前计算好位置,避免闪烁。

场景 2:动画前的准备

tsx 复制代码
function FadeIn({ children }) {
  const ref = useRef(null);

  useLayoutEffect(() => {
    // 设置初始状态(透明)
    ref.current.style.opacity = '0';
    
    // 下一帧开始动画
    requestAnimationFrame(() => {
      ref.current.style.transition = 'opacity 0.3s';
      ref.current.style.opacity = '1';
    });
  }, []);

  return <div ref={ref}>{children}</div>;
}

我的建议 :先用 useEffect,如果发现有视觉闪烁或抖动,再改成 useLayoutEffect。不要一上来就用 useLayoutEffect,它是同步的,会阻塞渲染,用多了会让页面卡顿。

3. useInsertionEffect - CSS-in-JS 专用

这个是 React 18 引入的,React 19 继续保留。老实说,除非你在写 CSS-in-JS 库(比如 styled-components、emotion),否则基本用不到。

tsx 复制代码
import { useInsertionEffect } from 'react';

function useCSS(rule) {
  useInsertionEffect(() => {
    // 在 DOM 更新前插入样式
    const style = document.createElement('style');
    style.textContent = rule;
    document.head.appendChild(style);
    
    return () => {
      document.head.removeChild(style);
    };
  }, [rule]);
}

执行顺序是:

复制代码
useInsertionEffect → useLayoutEffect → 浏览器绘制 → useEffect

普通开发者:基本不会用到,知道有这么个东西就行。

4. useEffect 的多种使用模式

模式 1:只在挂载时执行(componentDidMount)

tsx 复制代码
useEffect(() => {
  console.log('我只执行一次!');
  // 初始化操作
}, []); // 空依赖数组

模式 2:每次渲染都执行(componentDidUpdate)

tsx 复制代码
useEffect(() => {
  console.log('每次渲染都执行!');
  // 通常不推荐这样做
}); // 没有依赖数组

警告:这个要慎用!很容易造成性能问题。

模式 3:监听特定值变化

tsx 复制代码
useEffect(() => {
  console.log('count 变了!');
}, [count]); // 只在 count 变化时执行

模式 4:卸载时清理(componentWillUnmount)

tsx 复制代码
useEffect(() => {
  return () => {
    console.log('组件卸载了!');
    // 清理操作
  };
}, []);

实战场景详解

好了,理论说完了,咱们来看看实际项目中怎么用。

场景 1:数据请求

这是最常见的场景,没有之一。

tsx 复制代码
function ArticleList() {
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 这个 flag 很重要,防止组件卸载后还在 setState
    let isMounted = true;

    async function fetchArticles() {
      try {
        setLoading(true);
        const response = await fetch('/api/articles');
        const data = await response.json();
        
        // 只有组件还在时才更新状态
        if (isMounted) {
          setArticles(data);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchArticles();

    // 清理:标记组件已卸载
    return () => {
      isMounted = false;
    };
  }, []);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了:{error}</div>;

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

我踩过的坑 :之前没加 isMounted 标志,用户快速切换页面时会报错"Can't perform a React state update on an unmounted component"。加上这个标志后世界清净了。

场景 2:订阅和取消订阅

比如 WebSocket、EventBus、实时数据流等。

tsx 复制代码
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // 连接到聊天室
    const socket = new WebSocket(`wss://chat.example.com/${roomId}`);

    socket.onopen = () => {
      console.log(`已连接到房间 ${roomId}`);
    };

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    socket.onerror = (error) => {
      console.error('WebSocket 错误:', error);
    };

    // 清理:关闭连接
    return () => {
      console.log(`断开房间 ${roomId} 连接`);
      socket.close();
    };
  }, [roomId]); // roomId 变化时重新连接

  return (
    <div>
      <h2>聊天室 {roomId}</h2>
      <div className="messages">
        {messages.map((msg, index) => (
          <div key={index}>{msg.text}</div>
        ))}
      </div>
    </div>
  );
}

注意 :依赖数组里写了 roomId,所以当用户切换房间时,会先执行清理函数(关闭旧连接),然后再建立新连接。这个逻辑很优雅!

场景 3:定时器和轮询

tsx 复制代码
function StockPrice({ symbol }) {
  const [price, setPrice] = useState(null);

  useEffect(() => {
    // 立即获取一次
    fetchPrice();

    // 每 5 秒轮询一次
    const interval = setInterval(() => {
      fetchPrice();
    }, 5000);

    async function fetchPrice() {
      try {
        const res = await fetch(`/api/stock/${symbol}`);
        const data = await res.json();
        setPrice(data.price);
      } catch (error) {
        console.error('获取股价失败:', error);
      }
    }

    // 清理定时器
    return () => {
      clearInterval(interval);
    };
  }, [symbol]); // symbol 变化时重新轮询

  return (
    <div>
      {symbol}: ${price ?? '加载中...'}
    </div>
  );
}

场景 4:监听浏览器事件

tsx 复制代码
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    // 添加事件监听
    window.addEventListener('resize', handleResize);

    // 清理事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 只在挂载时添加,卸载时移除

  return size;
}

// 使用
function ResponsiveComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      窗口大小: {width} x {height}
      {width < 768 ? <MobileView /> : <DesktopView />}
    </div>
  );
}

场景 5:文档标题同步

tsx 复制代码
function useDocumentTitle(title) {
  useEffect(() => {
    const prevTitle = document.title;
    document.title = title;

    // 组件卸载时恢复原标题
    return () => {
      document.title = prevTitle;
    };
  }, [title]);
}

// 使用
function ArticlePage({ article }) {
  useDocumentTitle(article.title);

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

场景 6:表单自动保存

这个在实际项目中特别有用,用户体验会好很多。

tsx 复制代码
function AutoSaveForm() {
  const [formData, setFormData] = useState({
    title: '',
    content: ''
  });
  const [lastSaved, setLastSaved] = useState(null);

  // 防抖:内容变化 2 秒后自动保存
  useEffect(() => {
    const timer = setTimeout(() => {
      if (formData.title || formData.content) {
        saveToServer(formData);
        setLastSaved(new Date());
      }
    }, 2000);

    // 清理上一个定时器
    return () => clearTimeout(timer);
  }, [formData]); // formData 变化时重新设置定时器

  async function saveToServer(data) {
    try {
      await fetch('/api/drafts', {
        method: 'POST',
        body: JSON.stringify(data)
      });
      console.log('自动保存成功');
    } catch (error) {
      console.error('保存失败:', error);
    }
  }

  return (
    <form>
      <input
        value={formData.title}
        onChange={e => setFormData({ ...formData, title: e.target.value })}
        placeholder="标题"
      />
      <textarea
        value={formData.content}
        onChange={e => setFormData({ ...formData, content: e.target.value })}
        placeholder="内容"
      />
      {lastSaved && (
        <div className="save-status">
          最后保存于 {lastSaved.toLocaleTimeString()}
        </div>
      )}
    </form>
  );
}

优化技巧:这里每次 formData 变化都会清除旧定时器、设置新定时器,实现了防抖效果。用户停止输入 2 秒后才会真正发送请求,避免频繁请求服务器。

场景 7:滚动位置恢复

这个在列表页特别有用,用户从详情页返回时能回到之前的滚动位置。

tsx 复制代码
function ProductList() {
  const [products, setProducts] = useState([]);
  const scrollPos = useRef(0);

  useEffect(() => {
    // 恢复滚动位置
    window.scrollTo(0, scrollPos.current);

    // 监听滚动,保存位置
    function handleScroll() {
      scrollPos.current = window.scrollY;
    }

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  // 获取数据...

  return (
    <div className="product-list">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

场景 8:图片懒加载

tsx 复制代码
function LazyImage({ src, alt }) {
  const imgRef = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    // 创建 Intersection Observer
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect(); // 加载后就不再观察
        }
      },
      { threshold: 0.1 } // 10% 可见时触发
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    // 清理
    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <img
      ref={imgRef}
      src={isVisible ? src : '/placeholder.png'}
      alt={alt}
      style={{ minHeight: '200px' }}
    />
  );
}

常见错误和解决方案

错误 1:无限循环

tsx 复制代码
// ❌ 错误:会无限循环
function BadComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // 每次渲染都会触发
  }); // 没有依赖数组!

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

// ✅ 正确
function GoodComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(prev => prev + 1);
  }, []); // 只执行一次
}

错误 2:在清理函数中访问过期的状态

tsx 复制代码
// ❌ 可能有问题
function BadTimer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // count 是闭包中的旧值
    }, 1000);

    return () => clearInterval(timer);
  }, []); // count 不在依赖数组中
}

// ✅ 正确:使用函数式更新
function GoodTimer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1); // 使用前一个值
    }, 1000);

    return () => clearInterval(timer);
  }, []);
}

错误 3:忘记清理

tsx 复制代码
// ❌ 内存泄漏
function BadComponent() {
  useEffect(() => {
    const subscription = subscribeToData();
    // 忘记返回清理函数!
  }, []);
}

// ✅ 正确
function GoodComponent() {
  useEffect(() => {
    const subscription = subscribeToData();
    
    return () => {
      subscription.unsubscribe(); // 清理订阅
    };
  }, []);
}

错误 4:依赖数组不完整

tsx 复制代码
// ❌ ESLint 会警告
function BadSearch({ userId }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchUserData(userId); // 使用了 userId
  }, []); // 但依赖数组里没有!
}

// ✅ 正确
function GoodSearch({ userId }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchUserData(userId);
  }, [userId]); // 诚实地写上所有依赖
}

进阶技巧

技巧 1:自定义 Hook 封装生命周期逻辑

tsx 复制代码
// 封装数据请求逻辑
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    async function fetchData() {
      try {
        setLoading(true);
        const res = await fetch(url);
        const json = await res.json();
        
        if (isMounted) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchData();

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

  return { data, loading, error };
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return <div>{user.name}</div>;
}

技巧 2:组合多个生命周期

tsx 复制代码
function ComplexComponent() {
  // 挂载时执行
  useEffect(() => {
    console.log('组件挂载');
    return () => console.log('组件卸载');
  }, []);

  // 监听窗口大小
  const windowSize = useWindowSize();

  // 监听在线状态
  const isOnline = useOnlineStatus();

  // 自动保存
  useAutoSave(formData);

  return <div>...</div>;
}

技巧 3:条件性地执行副作用

tsx 复制代码
function SearchResults({ query, enabled }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 只在启用且有查询词时执行
    if (!enabled || !query) {
      return;
    }

    let isMounted = true;

    async function search() {
      const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
      if (isMounted) {
        setResults(data);
      }
    }

    search();

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

  return <div>...</div>;
}

性能优化建议

1. 避免在 useEffect 中创建新对象/数组

tsx 复制代码
// ❌ 每次都创建新对象
useEffect(() => {
  const options = { method: 'GET' }; // 新对象
  fetch(url, options);
}, [url, options]); // options 每次都不同,会无限循环

// ✅ 使用 useMemo
const options = useMemo(() => ({ method: 'GET' }), []);
useEffect(() => {
  fetch(url, options);
}, [url, options]);

2. 使用 AbortController 取消请求

tsx 复制代码
function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 创建 AbortController
    const controller = new AbortController();

    async function search() {
      try {
        const res = await fetch(`/api/search?q=${query}`, {
          signal: controller.signal // 传入 signal
        });
        const data = await res.json();
        setResults(data);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('请求被取消');
        } else {
          console.error('搜索失败:', err);
        }
      }
    }

    if (query) {
      search();
    }

    // 清理:取消请求
    return () => {
      controller.abort();
    };
  }, [query]);

  return (
    <div>
      <input 
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <div>{results.map(r => <div key={r.id}>{r.title}</div>)}</div>
    </div>
  );
}

用户快速输入时,旧的请求会被自动取消,避免资源浪费。

3. 合并多个状态更新

tsx 复制代码
// ❌ 多次渲染
useEffect(() => {
  setLoading(true);
  setError(null);
  setData(null);
}, []);

// ✅ 使用 useReducer 或对象状态
const [state, setState] = useState({
  loading: false,
  error: null,
  data: null
});

useEffect(() => {
  setState({ loading: true, error: null, data: null });
}, []);

调试技巧

1. 使用 console.log 追踪执行

tsx 复制代码
useEffect(() => {
  console.log('📌 useEffect 执行', { userId, timestamp: Date.now() });
  
  return () => {
    console.log('🧹 清理函数执行', { userId, timestamp: Date.now() });
  };
}, [userId]);

2. 使用 React DevTools Profiler

React DevTools 可以看到组件的渲染次数和原因,帮你找出性能问题。

3. 检查依赖数组

安装 eslint-plugin-react-hooks 插件,它会自动检查你的依赖数组是否完整。

bash 复制代码
npm install eslint-plugin-react-hooks --save-dev

总结

好了,说了这么多,我们来总结一下:

核心要点

  1. useEffect 是主力:99% 的生命周期需求都用它
  2. 依赖数组很重要:用了什么就写什么,别偷懒
  3. 记得清理:定时器、订阅、事件监听都要清理
  4. useLayoutEffect 慎用:只在需要同步 DOM 操作时用
  5. 封装自定义 Hook:复用逻辑,让代码更干净

最佳实践

tsx 复制代码
function BestPracticeComponent({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 1. 使用标志防止卸载后更新状态
    let isMounted = true;

    // 2. 使用 AbortController 取消请求
    const controller = new AbortController();

    async function fetchData() {
      try {
        const res = await fetch(`/api/data/${id}`, {
          signal: controller.signal
        });
        const json = await res.json();
        
        if (isMounted) {
          setData(json);
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      }
    }

    fetchData();

    // 3. 返回清理函数
    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [id]); // 4. 完整的依赖数组

  return <div>{data?.title}</div>;
}

我的心得

写了这么多年 React,生命周期这块其实就是要养成好习惯:

  • 该清理的一定要清理,不然会埋雷
  • 依赖数组要诚实,别跟 ESLint 对着干
  • 先想清楚再写,别上来就一顿操作
  • 多用自定义 Hook,让逻辑更清晰

希望这篇文章能帮到你!如果有什么问题,欢迎留言讨论。Happy coding! 🚀


参考资料:

写于 : 2025-12-09
最后更新: 2025-12-09

最后,欢迎访问我的个人网站: hixiaohezi.com

相关推荐
乔伊酱2 小时前
Bean Searcher 遇“鬼”记:为何我的查询条件偷偷跑进了 HAVING?
java·前端·orm
uu_code0072 小时前
字节磨皮算法详解
前端
HashTang2 小时前
【AI 编程实战】第 2 篇:让 AI 成为你的前端架构师 - UniApp + Vue3 项目初始化
前端·vue.js·ai编程
白中白121382 小时前
Vue系列-1
前端·javascript·vue.js
dorisrv2 小时前
Next.js 16 自定义 SVG Icon 组件实现方案 🎨
前端
用户新2 小时前
五万字沥血事件 深度学习 事件 循环 事件传播 异步 脱离新手区 成为事件达人
前端·javascript·事件·event loop
w2sfot2 小时前
JS代码压缩
前端·javascript·html
码途潇潇2 小时前
从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践
前端
import_random3 小时前
[python]miniconda(安装)
前端