【精通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 封装可复用逻辑单元
类组件过于复杂 改用函数组件 更简洁、易读、易测
相关推荐
十八朵郁金香2 小时前
深入解析:ES6 中 class 与普通构造器的区别
前端·ecmascript·es6
索迪迈科技2 小时前
CommonJS与ES6模块的区别
前端·ecmascript·es6
前端Hardy2 小时前
12个被低估的 CSS 特性,让前端开发效率翻倍!
前端·javascript·css
前端Hardy3 小时前
HTML&CSS:精美的3D折叠卡片悬停效果
前端·javascript·css
nightunderblackcat3 小时前
新手向:中文语言识别的进化之路
前端·javascript·easyui
Spider_Man3 小时前
打造属于你的前端沙盒 🎉
前端·typescript·github
用户47949283569153 小时前
🤫 你不知道的 JavaScript:`"👦🏻".length` 竟然不是 1?
前端·javascript·面试
掘金一周3 小时前
凌晨零点,一个TODO,差点把我们整个部门抬走 | 掘金一周 9.11
前端·人工智能·后端
用户8174413427483 小时前
kubernetes核心概念 Service
前端