React 生命周期

585 阅读12分钟

深入理解 React 的生命周期及 Hooks 对于构建高效、可维护的应用至关重要。随着 React Hooks 的引入,函数组件不仅具备了类组件的所有功能,还带来了更简洁、可复用和灵活的代码结构。本文将全面解析 React 的生命周期及 Hooks,帮助你深入理解它们的工作原理、应用场景及最佳实践,并与 Vue 3 的生命周期及组合式 API 进行对比,以便 Vue 3 开发者更好地掌握 React 的核心概念。


目录

  1. React 类组件的生命周期
  1. React 函数组件的生命周期及 Hooks
  1. Hooks 模拟生命周期方法的详解
  1. React 生命周期与 Vue 3 生命周期的对比
  1. Hooks 的最佳实践
  1. 常见问题与误区
  1. 总结

1. React 类组件的生命周期

在 React 中,类组件通过生命周期方法管理组件的不同阶段。生命周期方法分为挂载(Mounting)、更新(Updating)、卸载(Unmounting)和错误处理(Error Handling)四个主要阶段。

1.1 挂载阶段

挂载阶段是指组件被创建并插入到 DOM 中的过程。主要涉及以下生命周期方法:

  • constructor(props)
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
  
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>

        <button onClick={this.handleClick}>Increase</button>

      </div>

    );
  }
}
    • 用于初始化状态和绑定方法。
    • 在组件挂载之前调用一次。
  • static getDerivedStateFromProps(props, state)
static getDerivedStateFromProps(props, state) {
  if (props.initialCount !== state.count) {
    return { count: props.initialCount };
  }
  return null;
}
    • 在挂载和更新阶段均会被调用。
    • 根据 props 更新 state,通常用于从 props 派生 state。
  • render()
    • 唯一必须实现的方法。
    • 返回要渲染的 JSX。
  • componentDidMount()
componentDidMount() {
  fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => this.setState({ data }));
}
    • 在组件挂载到 DOM 后立即调用。
    • 适用于数据获取、订阅等副作用操作。

1.2 更新阶段

更新阶段发生在组件的 props 或 state 发生变化时,导致组件重新渲染。涉及以下生命周期方法:

  • static getDerivedStateFromProps(props, state)
    • 同挂载阶段,用于根据新的 props 更新 state。
  • shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate(nextProps, nextState) {
  return nextState.count !== this.state.count;
}
    • 控制组件是否需要重新渲染。
    • 返回 false 可以阻止更新,优化性能。
  • render()
    • 与挂载阶段相同,负责渲染组件。
  • getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate(prevProps, prevState) {
  if (prevState.count < this.state.count) {
    return this.myRef.scrollHeight;
  }
  return null;
}
    • 在 DOM 更新前调用。
    • 可用于获取 DOM 状态(如滚动位置),返回值会作为 componentDidUpdate 的第三个参数。
  • componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate(prevProps, prevState, snapshot) {
  if (snapshot !== null) {
    this.myRef.scrollTop = snapshot;
  }
}
    • 在组件更新后调用。
    • 可用于操作 DOM 或进行进一步的数据请求。

1.3 卸载阶段

卸载阶段是指组件从 DOM 中移除的过程。主要涉及以下生命周期方法:

  • componentWillUnmount()
componentWillUnmount() {
  clearInterval(this.timer);
}
    • 在组件卸载前调用。
    • 用于清理副作用,如取消订阅、清除定时器等。

1.4 错误处理阶段

错误处理阶段用于捕获渲染过程中、生命周期方法或子组件中的错误。涉及以下生命周期方法:

  • static getDerivedStateFromError(error)
static getDerivedStateFromError(error) {
  return { hasError: true };
}
    • 在渲染过程中抛出错误时调用。
    • 用于更新 state,以展示回退的 UI。
  • componentDidCatch(error, info)
componentDidCatch(error, info) {
  logErrorToService(error, info);
}
    • 捕获错误并执行副作用,如记录错误日志。

2. React 函数组件的生命周期及 Hooks

React Hooks 是 React 16.8 引入的功能,允许在函数组件中使用状态和其他 React 特性。通过 Hooks,函数组件可以管理状态、处理副作用、引用 DOM 元素等,几乎具备了类组件的所有功能。

2.1 Hooks 概述

Hooks 是一组能够让你在函数组件中“钩入” React 特性的函数。主要的 Hooks 包括:

  • useState:管理状态。
  • useEffect:处理副作用。
  • useContext:消费上下文。
  • useReducer:管理复杂状态逻辑。
  • useRef:引用 DOM 元素或存储可变值。
  • useMemo useCallback:性能优化。
  • 自定义 Hooks:复用组件逻辑。

2.2 useEffect

useEffect 是最常用的 Hook,用于在函数组件中处理副作用,如数据获取、订阅、手动操作 DOM 等。它可以模拟类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法。

2.2.1 useEffect 的基本用法

示例:

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

function FetchData({ url }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 类似于 componentDidMount 和 componentDidUpdate
    fetch(url)
      .then(response => response.json())
      .then(data => setData(data));
  }, [url]); // 依赖项数组

  if (!data) return <p>Loading...</p>;

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

export default FetchData;

解释:

  • useEffect 在组件渲染后执行。
  • 依赖项数组 [url] 表示当 url 变化时重新执行副作用。
  • 初始挂载和依赖项变化都会触发副作用。
2.2.2 清理副作用

useEffect 可以返回一个清理函数,用于在组件卸载或在下一次副作用执行前清理上一次的副作用。

示例:

import React, { useEffect } from 'react';

function Timer() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // 清理函数
    return () => clearInterval(timer);
  }, []); // 空依赖项数组,仅在挂载和卸载时执行

  return <div>Timer is running. Check console for ticks.</div>;
}

export default Timer;

解释:

  • 副作用在组件挂载时执行,设置一个定时器。
  • 返回的清理函数在组件卸载时调用,清除定时器。
2.2.3 依赖项数组

依赖项数组决定了 useEffect 何时执行副作用:

  • 无依赖项:每次渲染后都执行副作用。
  • 空数组 []:仅在挂载和卸载时执行副作用。
  • 特定依赖项:仅在依赖项变化时执行副作用。

示例:

useEffect(() => {
  // 每次渲染后执行
});

useEffect(() => {
  // 仅在挂载和卸载时执行
}, []);

useEffect(() => {
  // 依赖项变化时执行
}, [dependency1, dependency2]);

2.3 useLayoutEffect

useLayoutEffectuseEffect 类似,但它会在所有 DOM 变更之后、浏览器绘制之前同步执行。这使得它适用于需要同步测量 DOM 或在 DOM 变更前进行某些操作的场景。

示例:

import React, { useLayoutEffect, useRef } from 'react';

function MeasureDiv() {
  const divRef = useRef();

  useLayoutEffect(() => {
    const height = divRef.current.offsetHeight;
    console.log('Div height:', height);
  }, []);

  return <div ref={divRef}>Measure my height!</div>;
}

export default MeasureDiv;

注意:

  • useLayoutEffect 的执行时机可能会阻塞浏览器绘制,需谨慎使用。
  • 在大多数情况下,useEffect 足以满足需求,只有在需要同步读取布局时才使用 useLayoutEffect

2.4 useRef

useRef 用于在函数组件中创建一个可变的引用,该引用在组件的整个生命周期内保持不变。常用于访问 DOM 元素或存储任何可变值。

示例:

import React, { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>

    </div>

  );
}

export default TextInput;

用途:

  • 访问 DOM 元素:如获取输入框的值或焦点。
  • 存储可变值:不引起重新渲染的变量,如定时器 ID。

2.5 自定义 Hooks

自定义 Hooks 允许你将组件逻辑提取到可重用的函数中,增强代码的可复用性和可维护性。它们遵循命名约定,以 use 开头,并且可以组合现有的 Hooks 来实现复杂的逻辑。

示例:

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

// 自定义 Hook
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    
    // 清理函数
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return width;
}

function DisplayWidth() {
  const width = useWindowWidth();

  return <p>窗口宽度: {width}px</p>;
}

export default DisplayWidth;

优点:

  • 代码复用:在多个组件中复用相同的逻辑。
  • 组织逻辑:将相关逻辑聚合在一起,提高代码可读性。
  • 测试友好:易于单独测试自定义 Hooks。

3. Hooks 模拟生命周期方法的详解

通过 Hooks,函数组件可以模拟类组件的生命周期方法。理解这种模拟关系有助于更好地运用 Hooks 管理组件生命周期。

3.1 类组件 vs 函数组件

生命周期阶段类组件生命周期方法函数组件 Hooks
挂载constructor componentDidMountuseEffect(空依赖项数组)
更新componentDidUpdateuseEffect(依赖项变化)
卸载componentWillUnmountuseEffect(清理函数)
错误componentDidCatch getDerivedStateFromError无直接对应 Hooks(使用 Error Boundaries)

3.2 类组件生命周期方法与 Hooks 对比

3.2.1 componentDidMount vs useEffect(空依赖项数组)

类组件:

class MyComponent extends React.Component {
  componentDidMount() {
    // 执行副作用,如数据获取
  }

  render() {
    return <div>My Component</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 执行副作用,如数据获取
  }, []); // 空依赖项数组,类似 componentDidMount

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

export default MyComponent;
3.2.2 componentDidUpdate vs useEffect(依赖项变化)

类组件:

class MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.value !== prevProps.value) {
      // 执行副作用,如更新数据
    }
  }

  render() {
    return <div>Value: {this.props.value}</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent({ value }) {
  useEffect(() => {
    // 执行副作用,如更新数据
  }, [value]); // 仅当 value 变化时执行

  return <div>Value: {value}</div>;
}

export default MyComponent;
3.2.3 componentWillUnmount vs useEffect(清理函数)

类组件:

class MyComponent extends React.Component {
  componentDidMount() {
    this.timer = setInterval(() => {
      // 定时任务
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timer); // 清理定时器
  }

  render() {
    return <div>Timer</div>;
  }
}

函数组件:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      // 定时任务
    }, 1000);

    // 清理函数
    return () => clearInterval(timer);
  }, []); // 空依赖项数组,仅在挂载和卸载时执行

  return <div>Timer</div>;
}

export default MyComponent;
3.2.4 错误处理

类组件:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

函数组件:

目前,函数组件无法直接捕获错误。需要使用类组件作为错误边界。

示例:

import React from 'react';

class ErrorBoundary extends React.Component {
  // 同上
}

// 使用错误边界
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>

  );
}

注意: 未来 React 可能会引入 Hooks 来处理错误边界,但目前需要使用类组件。


4. React 生命周期与 Vue 3 生命周期的对比

对于熟悉 Vue 3 的开发者而言,理解 React 生命周期与 Vue 3 生命周期之间的异同,有助于更快地适应 React 的开发模式。

4.1 挂载阶段

React 类组件React 函数组件Vue 3
constructoruseState 初始化状态setup 初始化状态
componentDidMountuseEffect(空依赖)onMounted

示例(Vue 3):

<script setup>
import { ref, onMounted } from 'vue';

const count = ref(0);

onMounted(() => {
  console.log('组件已挂载');
});
</script>

<template>
  <div>Count: {{ count }}</div>

</template>

4.2 更新阶段

React 类组件React 函数组件Vue 3
componentDidUpdateuseEffect(依赖变化)watch / onUpdated

示例(Vue 3):

<script setup>
import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`);
});
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>

    <button @click="count++">Increase</button>

  </div>

</template>

4.3 卸载阶段

React 类组件React 函数组件Vue 3
componentWillUnmountuseEffect(清理函数)onBeforeUnmount

示例(Vue 3):

<script setup>
import { ref, onBeforeUnmount } from 'vue';

const timer = ref(null);

timer.value = setInterval(() => {
  console.log('Tick');
}, 1000);

onBeforeUnmount(() => {
  clearInterval(timer.value);
});
</script>

<template>
  <div>Timer is running. Check console for ticks.</div>

</template>

4.4 错误处理

React 类组件React 函数组件Vue 3
componentDidCatch无直接对应errorCaptured

示例(Vue 3):

<script setup>
import { defineEmits } from 'vue';

const emit = defineEmits(['errorCaptured']);

function handleError(error, info) {
  emit('errorCaptured', error, info);
}
</script>

<template>
  <div>
    <!-- 子组件可能触发错误 -->
  </div>

</template>

注意: Vue 3 支持在组件内捕获错误并处理,类似于 React 的错误边界。


5. Hooks 的最佳实践

正确使用 Hooks 能提升代码质量、可维护性和性能。以下是一些在 React 函数组件中使用 Hooks 的最佳实践。

5.1 遵循 Hook 规则

  • 只在顶层调用 Hooks
    • 不要在循环、条件或嵌套函数中调用 Hooks。
  • 只在 React 函数组件或自定义 Hooks 中调用 Hooks
    • 不要在普通的 JavaScript 函数中调用 Hooks。

示例:

// 正确
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// 错误
function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0); // 不要在条件语句中调用 Hooks
  }
  // ...
}

5.2 依赖项的正确管理

  • 确保依赖项完整
    • useEffectuseCallbackuseMemo 等 Hooks 中,依赖项数组应包含所有在副作用中使用的外部变量。
  • 避免不必要的依赖项
    • 使用稳定的函数引用,如 useCallback 缓存回调函数,避免因函数引用变化导致副作用频繁执行。

示例:

useEffect(() => {
  fetchData(userId);
}, [userId]); // 确保包含所有依赖项

5.3 避免滥用 Hooks

  • 不要过度分解 Hooks
    • 过度分解可能导致代码难以理解和维护。
  • 保持 Hooks 简单
    • 每个 Hook 应专注于单一职责,便于复用和测试。

5.4 使用自定义 Hooks 复用逻辑

  • 提取可复用的逻辑到自定义 Hooks
    • 如数据获取、表单处理等逻辑,可以通过自定义 Hooks 在多个组件中复用。

示例:

// useFetch.js
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let isMounted = true;
    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setLoading(false);
        }
      });
    return () => { isMounted = false };
  }, [url]);

  return { data, loading };
}

export default useFetch;
// DataDisplay.js
import React from 'react';
import useFetch from './useFetch';

function DataDisplay({ url }) {
  const { data, loading } = useFetch(url);

  if (loading) return <p>Loading...</p>;
  return <div>Data: {JSON.stringify(data)}</div>;
}

export default DataDisplay;

6. 常见问题与误区

6.1 useEffect 中的依赖项误用

问题:

  • 遗漏依赖项:可能导致副作用未能正确响应依赖项变化。
  • 不必要的依赖项:可能导致副作用频繁执行,影响性能。

解决方案:

  • 严格遵循 ESLint 插件:使用 eslint-plugin-react-hooks 检查依赖项。
  • 使用稳定的函数引用:通过 useCallback 缓存函数,避免因函数引用变化导致副作用频繁执行。

示例:

useEffect(() => {
  doSomething(count);
}, [count]); // 确保包含所有依赖项

6.2 多个 useEffect 的执行顺序

理解:

  • React 会按照 Hooks 的调用顺序依次执行所有的 useEffect
  • 每个 useEffect 的执行顺序与其在代码中的位置一致。

示例:

function MyComponent() {
  useEffect(() => {
    console.log('Effect 1');
  }, []);

  useEffect(() => {
    console.log('Effect 2');
  }, []);

  return <div>Check console</div>;
}

// 控制台输出顺序:
// Effect 1
// Effect 2

6.3 useLayoutEffect 的使用场景

理解:

  • useLayoutEffect 在 DOM 更新后、浏览器绘制前同步执行,适用于需要读取布局并同步触发重渲染的场景。
  • 不建议在性能敏感的应用中过度使用 useLayoutEffect,以避免阻塞浏览器绘制。

示例:

import React, { useLayoutEffect, useRef, useState } from 'react';

function LayoutEffectExample() {
  const divRef = useRef();
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    const newHeight = divRef.current.offsetHeight;
    setHeight(newHeight);
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ height: '100px', background: 'lightblue' }}>
        Measured Div
      </div>

      <p>Div Height: {height}px</p>

    </div>

  );
}

export default LayoutEffectExample;

7. 总结

React 的生命周期管理通过类组件的生命周期方法和函数组件的 Hooks 提供了强大的功能,允许开发者在不同阶段执行特定的操作。理解并正确使用这些生命周期方法和 Hooks 是构建高效、可维护 React 应用的关键。

关键要点:

  • 类组件生命周期方法:包括挂载、更新、卸载和错误处理四个阶段。
  • 函数组件 Hooks:通过 useEffectuseLayoutEffectuseRef 等 Hooks 实现生命周期管理。
  • Hooks 的规则:只在顶层调用 Hooks,只在 React 函数组件或自定义 Hooks 中调用 Hooks。
  • 依赖项管理:确保 useEffect 的依赖项数组完整,避免遗漏或不必要的依赖项。
  • 自定义 Hooks:提取可复用的逻辑,提高代码的可维护性和复用性。
  • 性能优化:合理使用 Hooks,如 useCallbackuseMemo,避免不必要的重新渲染和计算。
  • 与 Vue 3 的对比:理解 React 和 Vue 3 在生命周期管理上的异同,有助于快速上手 React。

通过本文的深入解析,你应该能够全面掌握 React 的生命周期及 Hooks,并将这些知识应用于实际项目中,构建出高效、可维护的 React 应用。