React 组件进化实录:从类组件到 Hooks 的实战拆解

99 阅读6分钟

前言:为什么要关注这次变革

刚学 React 那会儿,我最大的困惑不是JSX,也不是虚拟DOM,而是:为什么写个组件要这么多“仪式感”? classconstructorthis.statebind(this) 一套下来,总有种“只是想点个按钮,却要先填三张表”的感觉。后来 Hooks 出现,我才意识到,这并不是我水平不够,而是 React 自己也在进化。

一、类组件时代:结构与痛点

在 Hooks 出现之前,组件几乎等同于 class。你要状态、要副作用、要生命周期,全都得写在一个类里,这种写法本身没错,但对初学者并不友好。

1.1 状态管理:this.statesetState

在类组件里,如果一个变量希望“改了就刷新页面”,那它就不能是普通变量,必须老老实实放进 this.state,并通过 setState 修改。

export default class App extends Component {
  constructor() {
    super()
    this.state = { count: 0 }
  }

  add() {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <button onClick={this.add.bind(this)}>
        {this.state.count}
      </button>
    )
  }
}

这套机制本身是严谨的,但问题在于:
你在还没真正理解“状态是什么”之前,就被迫理解了 this、bind 和 class。

1.2 生命周期:分散的副作用管理

类组件的生命周期,本质上是在描述组件的一生,最常用的无非这三个:

  • componentDidMount:组件第一次渲染完成,适合发请求、开定时器
  • componentDidUpdate:状态或 props 更新后触发,适合同步变化
  • componentWillUnmount:组件卸载前,清理定时器、解绑事件
import React, { Component } from 'react';

export default class App3 extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [],
      count: 0,
    };
    this.timer = null;
    this.controller = new AbortController();
  }

  // 组件挂载完成:请求数据 + 初始化副作用
  componentDidMount() {
    fetch('https://mock-api.com/work/list', {
      signal: this.controller.signal,
    })
      .then(res => res.json())
      .then(data => {
        const list = data?.list || [];
        this.setState({ list, count: list.length });
      });

    this.timer = setInterval(() => {
      console.log('count:', this.state.count);
    }, 3600000);
  }

  // 组件更新完成:基于变化执行副作用
  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log('count changed:', this.state.count);
    }
  }

  // 组件卸载前:清理副作用
  componentWillUnmount() {
    clearInterval(this.timer);
    this.controller.abort();
  }

  add = () => {
    this.setState(state => ({ count: state.count + 1 }));
  };

  render() {
    const { list, count } = this.state;
    return (
      <div>
        <p>count: {count}</p>
        <button onClick={this.add}>add</button>
        <ul>
          {list.map((item, i) => (
            <li key={i}>{item}</li>
          ))}
        </ul>
      </div>
    );
  }
}

componentDidMount:组件第一次挂载时执行,这里用于发起 fetchlist 并用 setState 更新,同时启动 setInterval 定时任务;如果请求可能被中断,应保存 AbortController 以便后来取消。

componentDidUpdate:组件每次更新后都会触发,代码中只在 count(或 todoCount)真实变化时才执行后续操作,目的是避免无条件 setState 导致的无限循环。

componentWillUnmount:组件卸载前执行清理,包括 clearInterval 停掉定时器和用 abort() 取消未完成的网络请求,防止卸载后继续访问已销毁的组件状态或造成内存泄漏。

初始化、响应变化、清理三部分职责清晰,但问题也正出在这里:同一件事的“开始、变化、结束”被拆散在不同方法里,维护成本大大增加。
刚学时的我就很头疼了——逻辑是懂的,但就是不知道该去哪一段代码。

二、函数组件与 Hooks:新的表达方式

Hooks 出现后,函数组件不再只是“展示组件”,而是正式拥有了状态和副作用的能力,而且写法直观得多。

2.1 useState:把状态交给 React 管理

很多人第一次写函数组件都会踩这个坑:

export default function App() {
  let count = 0
  function add() {
    count++
  }
  return <button onClick={add}>{count}</button>
}

不管点多少次,页面永远是 0。
原因其实很简单:变量的值虽然变了,但 React 并不知道这次变化意味着组件需要重新渲染。

这时 useState 才真正登场。

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function add() {
    setCount(count + 1)
  }

  return <button onClick={add}>{count}</button>
}

你可以把 useState 理解成一句话: “这个值我交给 React 管,你帮我记住,也帮我决定什么时候重渲染。”

2.2 useEffect:按依赖聚合副作用(含依赖数组作用简述)

在类组件里,你要分别记住三个生命周期;
在 Hooks 里,useEffect 用“依赖关系”统一解决了这些问题。

import { useEffect, useState } from 'react';

export default function App2() {
  const [list, setList] = useState([]);
  const [count, setCount] = useState(0);

  // 只在组件首次渲染后执行:用于初始化数据
  useEffect(() => {
    fetch('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer')
      .then(res => res.json())
      .then(data => {
        setList(data.data || []);
      });
  }, []);

  // 依赖 count:只有 count 发生变化才会执行
  useEffect(() => {
    console.log('count 发生变化:', count);
  }, [count]);

  // 创建副作用,并在组件卸载前清理
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('timer running');
    }, 1000);

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

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <ul>
        {list.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

useEffect 的第二个参数 [中括号内] 用来声明这个副作用依赖哪些状态
当传入 [] 时,表示不依赖任何状态,只在组件首次渲染后执行一次;当传入 [count] 时,表示只有 count 发生变化,这个副作用才会再次执行。
换句话说,依赖数组决定什么时候该重新跑这段逻辑

useEffect 并不是简单地“替代生命周期”,而是通过依赖数组明确告诉 React:在什么条件下执行副作用,以及什么时候需要清理它。初始化、响应变化和收尾逻辑不再被强制拆散,而是按“关注的条件”自然聚合。

三、演进价值:Hooks 带来的改进

在类组件中,组件必须写成 class,状态集中放在 this.state 里,事件方法还需要手动处理 this 的指向。代码虽然规范,但样板代码偏多,真正的业务逻辑往往被这些结构性写法包裹住,读代码时需要先适应“类组件的语法规则”。

Hooks 出现之后,组件可以直接写成普通函数,状态通过 useState 拆分成一个个独立的变量,哪里用、哪里声明,逻辑关系更加直观。事件函数不再需要关心 this,组件本身更像是在描述“当前状态下页面应该长什么样”。

在副作用处理上,类组件需要把相关逻辑分散到不同的生命周期中,而 Hooks 通过 useEffect 按依赖条件聚合逻辑,让“因为什么变化、执行什么副作用”变得一眼可读,也更容易抽离和复用。

整体来看,Hooks 并不是让代码“更短”,而是减少了思考路径:你不再需要在多个生命周期之间来回跳转,而是围绕状态和依赖去理解组件行为,心智负担自然就降了下来。


最后

React 从类组件走向 Hooks,并不是“推翻重来”,而是一次工程体验上的优化。类组件帮我们理解了状态、生命周期和副作用的直观认知, Hooks 则让这些概念用更直观的方式表达出来。

如果你现在还在学类组件,完全不用焦虑;
等你理解 Hooks,再回头看 class,你反而会觉得:
原来当年的复杂,是有原因的。

这篇文章是我在学习过程中的一次阶段性总结,如果哪里理解有偏差,也欢迎指出。React 的路还长,慢慢走,总会走顺。