【精通react】(五)react 函数时组件为什么需要 hooks?

一、背景:函数式组件曾经的局限

在 React 16.8 之前(即 Hooks 出现前),React 组件有两种写法:

全屏复制

类型 特点
类组件(Class Component) 可以使用状态(state)、生命周期方法、ref 等
函数式组件(Function Component) 只能接收 props,返回 JSX,无法管理状态或生命周期

❌ 问题来了:

  • 函数式组件虽然简洁,但太"被动"了 ------ 它只是一个纯函数。
  • 所有复杂逻辑(如数据获取、状态更新、副作用处理)都必须用类组件实现。
  • 导致大量"简单 UI + 复杂逻辑"的组件被迫写成类组件,代码臃肿。

👉 比如一个按钮组件,想记录点击次数?必须写成类!

scala 复制代码
jsx
class ClickCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

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

  render() {
    return <button onClick={this.handleClick}>Clicked {this.state.count} times</button>;
  }
}

而理想中的写法应该是更简洁的函数形式。


🔧 二、Hooks 的诞生:让函数式组件"拥有能力"

React 团队提出了 Hooks 的概念 ------ 允许你在不编写 class 的情况下使用 state 以及其他 React 特性

✅ 目标:让函数式组件也能做类组件能做的事

于是有了:

  • useState → 使用状态
  • useEffect → 处理副作用(如数据请求、订阅、手动 DOM 操作)
  • useContext → 访问上下文
  • 还有数十个内置和自定义 Hook

现在上面的例子可以写成:

javascript 复制代码
jsx
import { useState } from 'react';

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

✅ 更短、更直观、没有 this 困扰。


🧩 三、为什么不能直接给函数组件加 state?------ 深层原因

你可能会问:

"为什么不直接让函数支持 this.state,像 class 一样?"

答案是:函数执行完就结束了,无法保存状态

⚠️ 函数执行机制 vs 类实例机制

全屏复制

对比项 函数式组件 类组件
执行方式 每次渲染都重新调用函数 实例化一次,多次调用 render() 方法
状态存储位置 无处可存 存在 this.state
this 是否存在 ❌ 不存在(或指向错误) ✅ 存在,指向实例

🌰 举例说明:

javascript 复制代码
js
function Counter() {
  let count = 0; // 局部变量
  return <div>{count}</div>;
}

每次渲染都会重新执行这个函数,count 被重置为 0,无法持久化。

而类组件:

scala 复制代码
js
class Counter extends Component {
  state = { count: 0 }; // 存在于实例上,不会随 render 丢失
}

所以,普通函数无法天然持有状态。必须有一个外部机制来"记住"上次的状态。


💡 四、Hooks 是如何解决这个问题的?------ 核心原理简析

React 实现了一个 状态记忆系统,它:

  1. 在首次渲染时,按顺序注册每个 Hook 的初始值;
  2. 在后续渲染中,按照相同的顺序读取之前保存的状态;
  3. 利用闭包 + 内部索引数组维护状态(简化理解)

🔄 示例:useState 如何工作?

javascript 复制代码
jsx
function Counter() {
  const [count, setCount] = useState(0); // 第1个 Hook
  const [name, setName] = useState('Alice'); // 第2个 Hook

  return (
    <div>
      {count}, {name}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

React 内部维护一个数组(伪代码):

ini 复制代码
js
const hooks = [];
let currentIndex = 0;

function useState(initialValue) {
  if (hooks[currentIndex] === undefined) {
    hooks[currentIndex] = initialValue;
  }
  const index = currentIndex;
  const value = hooks[index];
  const setValue = (newValue) => {
    hooks[index] = newValue;
    reRender();
  };
  currentIndex++;
  return [value, setValue];
}

⚠️ 所以要求:Hooks 必须始终以相同的顺序被调用,不能出现在条件语句中。


🎯 五、Hooks 带来的五大优势(为什么需要它)

全屏复制

优势 说明
逻辑复用更容易 以前靠 HOC 或 Render Props,嵌套深、难维护;现在用自定义 Hook 封装逻辑(如 useFetch, useLocalStorage
避免 this 的困扰 函数组件没有 this,不再需要绑定事件、担心作用域
更接近函数式编程思想 函数 + 纯净副作用控制,更利于测试和推理
组件逻辑组织更灵活 不再受限于生命周期函数,可以用多个 useEffect 分离不同逻辑
体积更小、性能更好 函数组件比类组件更轻量,Tree-shaking 更友好

📦 六、经典例子:对比三种写法

需求:加载用户数据并显示

1. 类组件写法(繁琐)

javascript 复制代码
jsx
class UserProfile extends Component {
  state = { user: null, loading: true };

  componentDidMount() {
    fetch('/api/user')
      .then(res => res.json())
      .then(user => this.setState({ user, loading: false }));
  }

  render() {
    const { user, loading } = this.state;
    if (loading) return <p>Loading...</p>;
    return <p>Hello, {user.name}</p>;
  }
}

2. 函数式 + Hooks(清晰简洁)

scss 复制代码
jsx
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, []); // 空依赖 → 相当于 componentDidMount

  if (loading) return <p>Loading...</p>;
  return <p>Hello, {user.name}</p>;
}

3. 自定义 Hook(进一步复用)

scss 复制代码
js
function useUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser)
      .finally(() => setLoading(false));
  }, []);

  return { user, loading };
}

然后在任意组件中使用:

javascript 复制代码
jsx
function UserProfile() {
  const { user, loading } = useUser();
  if (loading) return <p>Loading...</p>;
  return <p>Hello, {user.name}</p>;
}

✅ 真正实现了 UI 与逻辑分离、跨组件复用


⚠️ 七、Hooks 的规则(也是其设计约束)

为了保证上述机制正常工作,React 规定了两条重要规则:

  1. 只能在顶层调用 Hooks

    • 不能在循环、条件、嵌套函数中调用
    • 确保每次渲染调用顺序一致
  2. 只能在 React 函数组件或自定义 Hook 中调用

    • 不能在普通 JavaScript 函数中调用

这些规则由 ESLint 插件 eslint-plugin-react-hooks 自动检查。


✅ 八、总结:为什么函数式组件需要 Hooks?

全屏复制

问题 解决方案 Hooks 的角色
函数组件无法保存状态 useState 提供状态管理能力
函数组件无法处理副作用 useEffect 替代生命周期方法
逻辑难以复用 自定义 Hook 封装可复用逻辑单元
类组件过于复杂 改用函数组件 更简洁、易读、易测
相关推荐
runnerdancer8 小时前
LLM是怎么处理messages数组的,提示词缓存又是什么
前端·agent
陈随易9 小时前
VSCode的Copilot扩展支持接入DeepSeek,Kimi了!
前端·后端·程序员
我不是外星人10 小时前
有了 Harness Engineering ,真的还需要研发工程师吗?
前端·后端·ai编程
IT_陈寒13 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
Jackson__14 小时前
分享一个横向滚动案例,带悬停暂停,通用性很强
前端
MariaH14 小时前
git rebase的使用
前端
_柳青杨14 小时前
深入理解 JavaScript 事件循环
前端·javascript
阡陌Jony14 小时前
关于前端性能优化的一些问题:
前端
用户6000718191015 小时前
【翻译】简化 TSRX
前端
IT乐手16 小时前
佛德角逼平西班牙,国足还有啥借口?
前端