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
App 有 user 数据,必须一层一层传给 Layout、Sidebar最后到达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 的组件都会重渲染。
setTheme执行- 生成新的对象
{user, setUser, theme, setTheme}- Provider 的
value改变- 所有消费该 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>;
}