🧬 React 组件生命周期与并发模式课程设计

1. 课程概述

本课程旨在系统讲解 React 组件生命周期并发模式(Concurrent Mode) 的核心概念、应用场景及实战技巧。生命周期是组件从创建到销毁的完整过程,理解它能帮助我们在正确时机执行代码;并发模式是 React 18 的革命性特性,它能提升应用响应速度和用户体验。学生将学会如何利用生命周期方法管理组件行为,并运用并发特性构建更流畅的现代应用。

2. 课程目标

目标类型 描述
知识目标 理解 React 生命周期的三个阶段(挂载、更新、卸载)及每个阶段的常用方法。理解 Concurrent Mode 的设计理念、核心特性(时间切片、优先级调度、可中断渲染)及其对生命周期的影响。掌握 Hooks 如何模拟类组件的生命周期功能。理解 Error Boundary 的工作原理。
技能目标 能在类组件的生命周期方法中执行正确的操作(如数据请求、订阅、清理)。能使用 Hooks(useEffect, useLayoutEffect等) 在函数组件中管理副作用和生命周期逻辑。能使用 useTransitionuseDeferredValue 标记非紧急更新,优化用户体验。能创建 Error Boundary 组件捕获并处理错误。

3. 核心知识点与实例代码

3.1 React 生命周期基础

React 组件的生命周期可分为三个主要阶段:挂载(Mounting)更新(Updating)卸载(Unmounting),此外还有 错误处理 阶段。

类组件的生命周期方法及实战应用
阶段 方法 触发时机与用途 代码示例
挂载 constructor(props) 组件实例化时调用,用于初始化 state绑定事件方法 jsx class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } // ... }
static getDerivedStateFromProps(props, state) 渲染前调用,用于根据 props 的变化来更新 state。适用于少数罕见用例 jsx static getDerivedStateFromProps(props, state) { if (props.defaultValue !== state.value) { return { value: props.defaultValue }; } return null; }
render() 必须实现的方法,负责返回要渲染的 React 元素(JSX)。 jsx render() { return <div>{this.state.count}</div>; }
componentDidMount() 组件挂载到 DOM 后立即调用。是进行数据请求事件订阅DOM 操作的理想位置。 jsx componentDidMount() { this.fetchData(); // 数据请求 window.addEventListener('resize', this.handleResize); // 事件订阅 this.timer = setInterval(() => { this.tick(); }, 1000); // 设置定时器 }
更新 static getDerivedStateFromProps(props, state) 同挂载阶段。
shouldComponentUpdate(nextProps, nextState) 渲染前调用,通过返回 true/false 来决定组件是否重新渲染,是性能优化的重要手段。 ```jsx shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id
render() 同挂载阶段。
getSnapshotBeforeUpdate(prevProps, prevState) DOM 更新前调用,用于捕获更新前的 DOM 信息(如滚动位置)。返回值将作为参数传递给 componentDidUpdate jsx getSnapshotBeforeUpdate(prevProps, prevState) { if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; }
componentDidUpdate(prevProps, prevState, snapshot) 更新完成后立即调用。适合执行需要操作 DOM网络请求等副作用操作。 jsx componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } }
卸载 componentWillUnmount() 在组件卸载及销毁之前调用。在此执行必要的清理工作,如清除定时器取消网络请求清理订阅等,以防内存泄漏。 jsx componentWillUnmount() { clearInterval(this.timer); window.removeEventListener('resize', this.handleResize); this.controller.abort(); // 取消未完成的网络请求 }
错误处理 static getDerivedStateFromError(error) 在后代组件抛出错误后调用。接收错误信息,并返回一个值以更新 state,从而渲染降级/备用的 UI jsx static getDerivedStateFromError(error) { return { hasError: true }; }
componentDidCatch(error, errorInfo) 在后代组件抛出错误后调用。用于记录错误信息(如上报至监控系统)。 jsx componentDidCatch(error, errorInfo) { logErrorToService(error, errorInfo); }
函数组件的“生命周期”与 Hooks

函数组件本身没有生命周期方法,但可以通过 React Hooks 来模拟类组件生命周期的功能:

类组件生命周期 函数组件中的 Hooks 等效实现 代码示例
constructor useState 初始化 state jsx const [count, setCount] = useState(0);
componentDidMount useEffect(fn, []) jsx useEffect(() => { // 相当于 componentDidMount fetchData(); }, []);
componentDidUpdate useEffect(fn, [deps]) jsx useEffect(() => { // 在 count 变化后执行(包括首次挂载) console.log('Count updated:', count); }, [count]);
componentWillUnmount useEffect 的清理函数 jsx useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); // 清理函数 }, []);
shouldComponentUpdate React.memo, useMemo jsx const MyComponent = React.memo(({ value }) => { return <div>{value}</div>; });
getSnapshotBeforeUpdate & componentDidUpdate useLayoutEffect jsx useLayoutEffect(() => { // 在 DOM 更新后同步执行,可用于读取布局并触发重渲染 const { height } = node.getBoundingClientRect(); setHeight(height); });

综合示例:数据获取与清理(类组件 vs 函数组件)

// 类组件版本
class UserProfileClass extends React.Component {
  state = { user: null, loading: true };
  controller = new AbortController();

  async fetchUser() {
    try {
      const response = await fetch(`/api/users/${this.props.userId}`, { signal: this.controller.signal });
      const user = await response.json();
      this.setState({ user, loading: false });
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch error:', error);
      }
    }
  }

  componentDidMount() {
    this.fetchUser();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.setState({ loading: true });
      this.fetchUser();
    }
  }

  componentWillUnmount() {
    this.controller.abort();
  }

  render() {
    if (this.state.loading) return <div>Loading...</div>;
    return <div>{this.state.user.name}</div>;
  }
}

// 函数组件版本
function UserProfileFunctional({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`, { signal: controller.signal });
        const userData = await response.json();
        setUser(userData);
        setLoading(false);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Fetch error:', error);
        }
      }
    };

    fetchUser();

    return () => {
      controller.abort();
    };
  }, [userId]); // 依赖项:当 userId 变化时,重新执行 effect

  if (loading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

3.2 React 18 并发模式 (Concurrent Mode)

并发模式是 React 18 引入的一种新的底层架构,它使 React 能够同时准备多个版本的 UI,并可以中断渲染过程以处理更紧急的任务(如用户输入),从而提升应用的响应速度和用户体验。

并发模式的核心特性
  1. 时间切片 (Time Slicing):将长时间的渲染任务拆分成多个小块(通常为 5ms),在浏览器的空闲期执行,避免阻塞主线程,保证用户交互的流畅性。
  2. 可中断渲染 (Interruptible Rendering):渲染过程可以被更高优先级的更新中断。React 会暂停当前正在进行的渲染,先去处理更紧急的更新(如用户输入),然后再回来继续或重新开始之前的渲染。
  3. 优先级调度 (Lane Priority):React 为不同的更新分配了不同的优先级(例如:用户输入 > 动画更新 > 数据加载),确保高优先级的更新能更快地被处理并呈现在屏幕上。
并发模式下的 API 与实战

1. useTransition:标记非紧急更新
useTransition 允许你将一个更新标记为非紧急(低优先级),这样它就不会阻塞更紧急的更新(如用户输入或动画)。

import { useState, useTransition } from 'react';

function SearchBox() {
  const [keyword, setKeyword] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition(); // 返回一个待定状态标记和一个启动过渡函数的函数

  const handleChange = (e) => {
    const value = e.target.value;
    setKeyword(value); // 紧急更新:立即更新输入框的值

    startTransition(() => { // 非紧急更新:搜索结果可以稍后更新
      setResults(prevResults => {
        // 假设这是一个昂贵的过滤操作
        return prevResults.filter(item => item.name.includes(value));
      });
    });
  };

  return (
    <div>
      <input type="text" value={keyword} onChange={handleChange} />
      {/* isPending 表示过渡更新是否正在进行 */}
      {isPending ? <span>Loading...</span> : null}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

2. useDeferredValue:延迟更新某个值
useDeferredValue 接受一个值并返回该值的延迟版本。在紧急更新发生时,延迟版本的值会“滞后”于原始值,从而避免阻塞紧急更新。

import { useState, useDeferredValue, useMemo } from 'react';

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter); // deferredFilter 会“滞后”于 filter

  const filteredList = useMemo(() => {
    return products.filter(product => 
      product.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [products, deferredFilter]); // 计算依赖于 deferredFilter

  return (
    <div>
      <input 
        type="text" 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} // 紧急更新:输入框立即响应
        placeholder="Search products..."
      />
      {/* 列表渲染使用延迟后的值,可能会稍晚更新 */}
      <ul>
        {filteredList.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

3. Suspense 与并发模式
Suspense 允许组件在等待异步数据加载时**“暂停”渲染**并显示一个回退内容(如加载指示器)。在并发模式下,Suspense 可以更好地与可中断渲染结合。

import { Suspense } from 'react';

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading User Profile...</div>}>
        <UserProfile />
      </Suspense>
      <Suspense fallback={<div>Loading Product List...</div>}>
        <ProductList />
      </Suspense>
    </div>
  );
}

// 假设 UserProfile 和 ProductList 内部使用了一些异步数据获取库(如 Relay)或实现了 Suspense 的数据获取方式

3.3 错误边界 (Error Boundary)

错误边界是类组件,它捕获并处理其子组件树中发生的 JavaScript 错误,记录这些错误,并显示一个降级 UI 而不是崩溃的组件树。

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    this.logErrorToService(error, errorInfo); // 上报错误到监控服务
  }

  logErrorToService(error, info) {
    // 这里通常会上报到像 Sentry、LogRocket 这样的错误监控服务
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
          </details>
          <button onClick={() => this.setState({ hasError: false })}>Try again?</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ErrorBoundary>
      <MyComponentWhichMightThrow />
    </ErrorBoundary>
  );
}

4. 综合实战案例:并发模式下的图片画廊

下面是一个融合了生命周期管理、并发模式特性和错误处理的综合示例:

import React, { useState, useEffect, useTransition, useDeferredValue, Suspense } from 'react';

// 一个模拟异步加载图片的组件,可能会抛出错误
function AsyncImage({ src, alt }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const img = new Image();
    img.src = src;
    img.onload = () => setIsLoaded(true);
    img.onerror = () => setError(new Error(`Failed to load image: ${src}`));

    return () => {
      img.onload = null;
      img.onerror = null;
    };
  }, [src]);

  if (error) throw error; // 抛出错误让 Error Boundary 捕获
  if (!isLoaded) return <div className="image-placeholder">Loading...</div>;

  return <img src={src} alt={alt} />;
}

// 主组件
function ImageGallery() {
  const [query, setQuery] = useState('');
  const [images, setImages] = useState([]);
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);

  // 根据 deferredQuery 过滤图片(非紧急计算)
  const filteredImages = React.useMemo(() => {
    return images.filter(image => 
      image.tags.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [images, deferredQuery]);

  // 组件挂载时获取图片数据
  useEffect(() => {
    const fetchImages = async () => {
      try {
        // 模拟 API 调用
        const response = await fetch('/api/images');
        const data = await response.json();
        setImages(data);
      } catch (error) {
        console.error('Failed to fetch images:', error);
      }
    };

    fetchImages();
  }, []);

  const handleSearchChange = (e) => {
    const value = e.target.value;
    setQuery(value); // 紧急更新:立即更新输入框

    // 标记接下来的状态更新为非紧急
    startTransition(() => {
      // 这里可以设置一些依赖于搜索词的衍生状态
      // 但 filteredImages 的计算已经由 useDeferredValue 和 useMemo 处理
    });
  };

  return (
    <div>
      <input 
        type="text" 
        value={query} 
        onChange={handleSearchChange} 
        placeholder="Search images by tags..."
      />
      {isPending && <span className="search-indicator"> (Searching...) </span>}
      
      <div className="image-grid">
        <Suspense fallback={<div>Loading gallery...</div>}>
          {filteredImages.map(image => (
            <div key={image.id} className="image-item">
              <ErrorBoundary fallback={<div>Could not load this image.</div>}>
                <AsyncImage src={image.url} alt={image.title} />
                <p>{image.title}</p>
              </ErrorBoundary>
            </div>
          ))}
        </Suspense>
      </div>
    </div>
  );
}

// 简化版的 Error Boundary(实际应用中可能更复杂)
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    console.log('Error caught by Image Error Boundary:', error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

案例技能点解析

  • 生命周期管理useEffect 用于数据获取(componentDidMount),AsyncImage 内部的 useEffect 用于图片加载和清理(componentWillUnmount)。
  • 并发模式useTransitionuseDeferredValue 共同协作,确保搜索输入框的响应是即时且流畅的,而昂贵的过滤操作和图片渲染可以被延迟或中断。
  • 错误处理ErrorBoundary 类组件捕获子组件(AsyncImage)中抛出的错误,防止整个画廊因一张图片加载失败而崩溃。
  • Suspense:用于包裹整个图片列表,提供加载中的回退UI。

5. 课后练习与思考

  1. 基础练习

    • 将一个使用 componentDidMountcomponentWillUnmount 的类组件改写为使用 useEffect 的函数组件。
    • 使用 useTransition 优化一个按钮点击后需要执行大量计算的状态更新。
  2. 进阶挑战

    • 实现一个自定义 Hook useFetch,它接受一个 URL,并返回 dataloadingerror 状态,并在组件卸载时自动取消未完成的请求。
    • 构建一个复杂的列表页面,集成搜索(使用 useDeferredValue)、过滤、排序功能,并确保用户交互始终流畅。
  3. 思考题

    • 在并发模式下,componentDidMountcomponentDidUpdate 等生命周期方法的行为可能会发生什么变化?开发者需要注意什么?
    • 为什么说 useLayoutEffect 更适合处理 DOM 测量和动画?它与 useEffect 在执行时机上有何不同?
    • 错误边界为什么不能捕获其自身内部的错误、异步代码中的错误以及事件处理函数中的错误?

更多推荐