【React之Hooks原理、组件、状态管理浅谈】

Hooks 原理:链表 + 闭包

Hooks 的存储结构(useState底层)

javascript 复制代码
// React 内部(简化版)
let currentFiber = null;
let hookIndex = 0;

function useState(initialValue) {
  const fiber = currentFiber;//获取当前组件的Fiber
  const hooks = fiber.hooks || (fiber.hooks = []); // 每个组件的 hooks 数组
  const hook = hooks[hookIndex] || { state: initialValue, queue: [] };//hook对象
  
  // 处理更新队列
  hook.queue.forEach(action => {
    hook.state = typeof action === 'function' ? action(hook.state) : action;
  });
  hook.queue = [];
  
  const setState = (action) => {
    hook.queue.push(action);
    scheduleUpdate(fiber); // 触发重新渲染
  };
  
  hooks[hookIndex] = hook;
  hookIndex++;
  
  return [hook.state, setState];
}

// 渲染组件时
function renderComponent(fiber) {
  currentFiber = fiber;
  hookIndex = 0; // 重置索引
  const result = fiber.type(fiber.props); // 执行函数组件
  currentFiber = null;
  return result;
}
  • Fiber :是整个组件的 React 内部节点(代表一个组件 / 元素实例)
  • Hooks 链表 / 数组 :是挂载在 Fiber 上的一个附属属性fiber.hooks
  • Hooks 本身 :才是这个链表 / 数组里的真正节点

可以把它们的关系理解成:

javascript 复制代码
一个 Fiber 节点(组件实例)
  ↓ 包含
  一个 hooks 数组/链表(存储该组件的所有 Hook)
     ↓ 每个元素
     一个 Hook 对象({ state, queue })

拆解上述代码方便理解:

javascript 复制代码
// 1. currentFiber = 当前正在渲染的【组件】
const fiber = currentFiber; 

// 2. fiber.hooks = 这个组件【专属的 Hook 存储数组】
const hooks = fiber.hooks || (fiber.hooks = []); 

// 3. hook = 数组里的【单个 Hook 对象】
const hook = hooks[hookIndex] || { state: initialValue, queue: [] };

第一次渲染完整流程

开始渲染组件:

javascript 复制代码
renderComponent(fiber)

设置全局指针:

javascript 复制代码
currentFiber = fiber
hookIndex = 0

执行组件函数:

javascript 复制代码
fiber.type(fiber.props) → 执行你的组件

组件内部遇到第一个 useState:

javascript 复制代码
const [a, setA] = useState(10)

进入 useState:

javascript 复制代码
hookIndex = 0
hooks 数组是空的
创建 hook: { state:10, queue:[] }
存到 hooks[0]
hookIndex 变成 1
返回 [10, setA]

遇到第二个 useState:

javascript 复制代码
const [b, setB] = useState(20)

进入 useState:

javascript 复制代码
hookIndex = 1
创建 hook: { state:20, queue:[] }
存到 hooks[1]
hookIndex 变成 2
返回 [20, setB]

组件执行完毕:hooks 数组:

javascript 复制代码
[
  { state:10, queue:[] },
  { state:20, queue:[] }
]

渲染结束:

javascript 复制代码
currentFiber = null

更新流程(点击按钮 → setState)
javascript 复制代码
setA(100)

把更新加入队列:

javascript 复制代码
hook.queue.push(100)

触发重新渲染:

javascript 复制代码
scheduleUpdate(fiber)

再次执行 renderComponent (fiber):

javascript 复制代码
hookIndex = 0
执行组件函数

第一次 useState:

javascript 复制代码
hook = hooks[0]

执行队列:

javascript 复制代码
queue = [100]
执行 → hook.state = 100
清空 queue

返回最新 state:100

javascript 复制代码
return [100, setA]

第二个 useState 正常执行:

javascript 复制代码
返回 [20, setB]

渲染完成,页面更新。


Q&A:

fiber.type (fiber.props) 为什么这么写?

因为**fiber.type** 存的就是你的组件函数,这是底层调用组件的方式。

为什么要分为"存进队列"和"出发渲染之后更新队列"两步?

因为React要做批量更新、合并多次setState等下一次渲染时全部执行、性能优化。

第一次渲染到底发生了什么?
  • 索引归零
  • 执行组件
  • 按顺序创建 hook 存入数组
  • 渲染完成
更新队列怎么实现?
  • setState 不直接更新
  • 把任务放进队列
  • 下次渲染时一次性全部执行
怎么触发重新渲染?
  • setState → 通知 React
  • React 再次调用 renderComponent
  • 组件重新执行
  • 拿到新状态

在这里若是还有不清楚,请往下看。


Fiber真实结构:

javascript 复制代码
Fiber = {
  type: 组件函数,
  props: {},
  // ✅ 重点:hooks 是 Fiber 的一个属性,是数组/链表
  hooks: [ 
    { state: 0, queue: [] },  // Hook1:useState(0)
    { state: 0, queue: [] }   // Hook2:useState(0)
  ]
}

为什么 Hooks 不能在条件语句中使用?

javascript 复制代码
function Component({ condition }) {
  // ❌ 错误:条件语句中使用 Hook
  if (condition) {
    const [state, setState] = useState(0); // hookIndex = 0
  }
  const [count, setCount] = useState(0); // hookIndex = 0 或 1?
  
  // 第一次渲染(condition = true):
  // hooks[0] = state
  // hooks[1] = count
  
  // 第二次渲染(condition = false):
  // hooks[0] = count ❌ 类型错误!React 期望 hooks[0] 是 state
}

核心原因:React 靠【调用顺序】匹配 Hook,不靠名字。 React 强制:所有 Hook 必须在组件顶层、不能在 if/for/ 嵌套函数里使用

正确做法:

javascript 复制代码
function Component({ condition }) {
  const [state, setState] = useState(0);
  const [count, setCount] = useState(0);
  
  // ✅ 在 Hook 外部使用条件
  if (condition) {
    // 使用 state
  }
}

函数组件 vs 类组件

核心区别:

特性 类组件 函数组件
状态管理 this.state useState
生命周期 componentDidMount 等 useEffect
this 绑定 需要 bind 无 this
性能 略重 更轻量
闭包陷阱

闭包陷阱:函数组件的坑

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // ❌ 永远打印 0
      setCount(count + 1); // ❌ 永远是 0 + 1
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 空依赖 → count 被"冻结"在初始值
  
  return <div>{count}</div>;
}

解决方案 1:函数式更新

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1); // ✅ 使用最新值
  }, 1000);
  
  return () => clearInterval(timer);
}, []); // 依赖数组仍为空

使用函数式更新的原理是:**绕过闭包取值,直接通过React中hook.state取值,**从而实现数字递增。深层逻辑原理就是上述Hooks的存储结构以及更新流程中的:

javascript 复制代码
对于定时器例子,
第一次useEffect执行:
setCount把函数塞进更新队列(React底层):

const setState = (action) => {
  hook.queue.push(action);    把更新塞进队列!
  scheduleUpdate(fiber);      触发重新渲染!
};

触发组件重渲染:
renderComponent(fiber); 重新跑一遍组件

之后处理更新队列:
  hook.queue.forEach(action => {
    hook.state = typeof action === 'function' ? action(hook.state) : action;
  });
这里的action就是c=>c+1,执行action(hook.state),此时的hook.state就是初始的0,之后更新状态。

网页第一次加载:

执行同步 JS;遇到 useEffect,把它存进 hooks 数组,暂时不执行;DOM 渲染完成;执行 useEffect 回调 ;创建定时器 setInterval;

1 秒后,定时器触发:执行 setCount(c => c + 1)、setCount把函数塞进更新队列、触发组件重新渲染;

重新渲染:执行组件函数遇到 useState、**执行队列里的函数(React底层)、**计算出新 state、返回最新值、页面更新。

之后流程:定时器里的setCount(c=>c+1)会被定时塞进队列,一秒执行一次,count一直增加。

解决方案 2:useRef 保存最新值

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  useEffect(() => {
    countRef.current = count; // 每次渲染更新 ref
  });
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(countRef.current); // ✅ 最新值
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>{count}</div>;
}

类组件的 this 问题

javascript 复制代码
class Counter extends React.Component {
  state = { count: 0 };
  
  // ❌ 错误:this 丢失
  handleClick() {
    this.setState({ count: this.state.count + 1 }); // this = undefined
  }
  
  // ✅ 解决方案 1:箭头函数
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  }
  
  // ✅ 解决方案 2:bind
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  
  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

状态管理:从Props Drilling到全局方案

Props Drilling属性钻孔

javascript 复制代码
App → Layout → Sidebar → UserProfile

Appuser 数据,必须一层一层传给 LayoutSidebar最后到达UserProfile,但 Layout 和 Sidebar 根本不用 user, 它们只是传过去而已。这就是属性钻孔。

缺点 :代码冗余,组件耦合,维护噩梦,层级越深越痛苦。

**解决方案1:**Context API(上下文)

创建一个 "公共存储空间",组件想拿就拿,不用一层一层传。

步骤:创建 Contex:

javascript 复制代码
const UserContext = createContext();

在顶层用 Provider 包裹,提供数据:

javascript 复制代码
<UserContext.Provider value={{ user, setUser }}>
  <Layout />
</UserContext.Provider>

底层组件直接用 useContext 拿

javascript 复制代码
const { user } = useContext(UserContext);
性能大坑:
javascript 复制代码
<UserContext.Provider value={{ user, setUser, theme, setTheme }}>
  <ComponentA />
  <ComponentB />
</UserContext.Provider>
javascript 复制代码
// A 只用 user
function ComponentA() {
  const { user } = useContext(UserContext);
}

// B 只用 theme
function ComponentB() {
  const { theme } = useContext(UserContext);
}

问题:**theme 改变时,ComponentA 也会重新渲染。**即使 ComponentA 根本没用到 theme。

为什么会这样?

因为:Context 监听的是整个 value 对象只要 value 变了,所有用了这个 Context 的组件都会重渲染。

  1. setTheme 执行
  2. 生成新的对象 {user, setUser, theme, setTheme}
  3. Provider 的 value 改变
  4. 所有消费该 Context 的组件全部重新渲染
    • ComponentA → 重渲染(没用 theme 也会!)
    • ComponentB → 重渲染
优化方案:拆分 Context
javascript 复制代码
const UserContext = createContext();
const ThemeContext = createContext();

function App() {
  const [user, setUser] = useState({ name: 'Alice' });
  const [theme, setTheme] = useState('light');
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <ComponentA />
        <ComponentB />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

解决方案 2:Redux

javascript 复制代码
// store.js
import { createSlice, configureStore } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: { name: 'Alice', age: 25 },
  reducers: {
    updateName: (state, action) => {
      state.name = action.payload; // Immer 自动处理不可变性
    },
    incrementAge: (state) => {
      state.age += 1;
    },
  },
});

export const { updateName, incrementAge } = userSlice.actions;
export const store = configureStore({
  reducer: { user: userSlice.reducer },
});

// App.js
import { Provider, useSelector, useDispatch } from 'react-redux';

function App() {
  return (
    <Provider store={store}>
      <UserProfile />
    </Provider>
  );
}

function UserProfile() {
  const user = useSelector(state => state.user); // 订阅状态
  const dispatch = useDispatch();
  
  return (
    <div>
      <p>{user.name} - {user.age}岁</p>
      <button onClick={() => dispatch(updateName('Bob'))}>改名</button>
      <button onClick={() => dispatch(incrementAge())}>+1岁</button>
    </div>
  );
}

Redux 性能优化:选择器

javascript 复制代码
// ❌ 每次都创建新对象 → 触发重新渲染
const data = useSelector(state => ({
  name: state.user.name,
  age: state.user.age,
}));

// ✅ 只订阅需要的字段
const name = useSelector(state => state.user.name);
const age = useSelector(state => state.user.age);

// ✅ 使用 reselect 缓存计算结果
import { createSelector } from 'reselect';

const selectUser = state => state.user;
const selectAdult = createSelector(
  [selectUser],
  user => user.age >= 18 // 只有 age 变化时才重新计算
);

function Component() {
  const isAdult = useSelector(selectAdult);
  return <div>{isAdult ? '成年' : '未成年'}</div>;
}

解决方案 3:Zustand(轻量级)

javascript 复制代码
import create from 'zustand';

// 创建 store
const useStore = create((set) => ({
  user: { name: 'Alice', age: 25 },
  updateName: (name) => set(state => ({ 
    user: { ...state.user, name } 
  })),
  incrementAge: () => set(state => ({ 
    user: { ...state.user, age: state.user.age + 1 } 
  })),
}));

// 使用
function UserProfile() {
  const user = useStore(state => state.user);
  const updateName = useStore(state => state.updateName);
  const incrementAge = useStore(state => state.incrementAge);
  
  return (
    <div>
      <p>{user.name} - {user.age}岁</p>
      <button onClick={() => updateName('Bob')}>改名</button>
      <button onClick={incrementAge}>+1岁</button>
    </div>
  );
}

// ✅ 性能优化:只订阅 name
function NameDisplay() {
  const name = useStore(state => state.user.name); // age 变化不会重新渲染
  return <div>{name}</div>;
}
相关推荐
楼田莉子4 小时前
CMake学习:CMake在静态库工程场景上应用
开发语言·c++·后端·学习·软件构建
csbysj20204 小时前
SVG 渐变 - 线性
开发语言
迷藏4944 小时前
**发散创新:用 Rust实现高效共识算法——从 Raft到自研轻量级协议的实战演进**
java·开发语言·rust·共识算法
wuqingshun3141594 小时前
说说你对spring MVC的理解
java·开发语言·jvm
wjs20244 小时前
JavaScript 测试 Prototype
开发语言
invicinble4 小时前
对于前端框架--vue-elemnt-admin这个框架的分析
前端·vue.js·前端框架
蜡台4 小时前
Vue 中directive的钩子函数 作用,调用时机,参数,及使用场景举例说明
前端·javascript·vue.js·指令·directive
梵得儿SHI4 小时前
Vue 3 生态工具实战:UI 组件库与表单验证完全指南
前端·ui·vue3·elementplus·表单验证·antdesignvue·veevalidate