我们看看React官网是怎么说的
Hooks ------以 use 开头的函数------只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 "use" React 特性,类似于在文件顶部"导入"模块。
这不得不谈react中状态的维护
- 局部变量无法在多次渲染中持久保存。 当 React 再次渲染这个组件时,它会从头开始渲染------不会考虑之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
要使用新数据更新组件,需要做两件事:
- 保留 渲染之间的数据。
- 触发 React 使用新数据渲染组件(重新渲染)。
看下react官网的:state组件的记忆中的一段代码
js
let componentHooks = [];
let currentHookIndex = 0;
// useState 在 React 中是如何工作的(简化版)
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// 这不是第一次渲染
// 所以 state pair 已经存在
// 将其返回并为下一次 hook 的调用做准备
currentHookIndex++;
return pair;
}
// 这是我们第一次进行渲染
// 所以新建一个 state pair 然后存储它
pair = [initialState, setState];
function setState(nextState) {
// 当用户发起 state 的变更,
// 把新的值放入 pair 中
pair[0] = nextState;
updateDOM();
}
// 存储这个 pair 用于将来的渲染
// 并且为下一次 hook 的调用做准备
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
-
全局状态:
componentHooks:一个数组,用于存储组件中所有 Hook 的状态对(state pair)。currentHookIndex:当前 Hook 的索引,用于在多次渲染之间跟踪每个 Hook 的顺序。
-
useState 函数:
- 参数:
initialState,即 state 的初始值。 - 首先,它检查
componentHooks在当前索引currentHookIndex处是否已经存在一个 state pair(即[state, setState])。 - 如果存在(说明不是第一次渲染),则直接返回该 pair,并将
currentHookIndex加一,以便下一个 Hook 使用。 - 如果不存在(第一次渲染),则创建一个新的 state pair,其中初始状态为
initialState,并且定义了一个setState函数。
- 参数:
-
setState 函数:
- 当调用 setState 时,它会更新 state pair 中的状态值(即
pair[0]设置为新的状态nextState)。 - 然后调用
updateDOM()函数(这个函数在代码中没有实现,但意在对组件进行重新渲染)。
- 当调用 setState 时,它会更新 state pair 中的状态值(即
-
存储 state pair:
- 在第一次渲染时,新创建的 state pair 会被存储到
componentHooks[currentHookIndex]中。 - 然后
currentHookIndex加一,以便下一个 Hook 使用。
- 在第一次渲染时,新创建的 state pair 会被存储到
-
返回 state pair:
- 返回数组
[state, setState],和 React 的 useState 返回的形式一致。
- 返回数组
这个简化版中假设当前只有一个组件,每次重新渲染时会按相同的顺序调用 Hook。它通过 currentHookIndex 来跟踪当前是第几个 Hook。在每次渲染开始时,currentHookIndex 应该被重置为0(但在这个代码片段中,重置操作没有显示,应该是在 updateDOM 函数中或其他地方处理)。
scss
// 必须保证每次渲染时 Hook 的调用顺序一致
const [name, setName] = useState('Alice'); // Hook 0
const [age, setAge] = useState(25); // Hook 1
- 通过索引顺序识别不同的 Hook
- 解释了为什么 React Hook 不能在条件语句中使用的根本原因
接着我们来继续举例
scss
// 有问题的组件 - 在条件语句中调用 Hook
function ProblematicComponent() {
const [count, setCount] = useState(0);
// 🚨 违反规则:在条件语句中调用 Hook
if (count > 2) {
const [extra, setExtra] = useState('extra state'); // 这个 Hook 有时调用,有时不调用
}
const [name, setName] = useState('Alice'); // 这个 Hook 的位置会变化
return { count, setCount, name, setName };
}
模拟渲染
//
function simulateRenders() {
console.log('=== 第一次渲染 ===');
componentHooks = [];
currentHookIndex = 0;
const instance1 = ProblematicComponent();
console.log('Hook 状态:', componentHooks.map(hook => hook[0]));
// 输出: [0, 'Alice']
console.log('\n=== 第二次渲染 (count=1) ===');
componentHooks = [];
currentHookIndex = 0;
instance1.setCount(1);
const instance2 = ProblematicComponent();
console.log('Hook 状态:', componentHooks.map(hook => hook[0]));
// 输出: [1, 'Alice'] - 正常
console.log('\n=== 第三次渲染 (count=3) ===');
componentHooks = [];
currentHookIndex = 0;
instance1.setCount(3);
const instance3 = ProblematicComponent();
console.log('Hook 状态:', componentHooks.map(hook => hook[0]));
// 输出: [3, 'extra state', 'Alice'] - 现在有3个Hook
console.log('\n=== 第四次渲染 (count=2) ===');
componentHooks = [];
currentHookIndex = 0;
instance1.setCount(2);
const instance4 = ProblematicComponent();
console.log('Hook 状态:', componentHooks.map(hook => hook[0]));
// 🚨 问题出现: [2, 'Alice'] - 但系统期望3个Hook,现在只有2个!
// 'Alice' 现在对应的是之前 'extra state' 的状态!
}
ruby
// 正确的调用顺序(始终一致):
// 渲染1: Hook0 → Hook1 → Hook2
// 渲染2: Hook0 → Hook1 → Hook2
// 渲染3: Hook0 → Hook1 → Hook2
// 错误的调用顺序(条件性调用):
// 渲染1: Hook0 → Hook1 // 条件为false,跳过Hook2
// 渲染2: Hook0 → Hook1 → Hook2 // 条件为true,添加Hook2
// 渲染3: Hook0 → Hook1 // 条件又为false,又跳过Hook2
状态错配的具体过程
function
console.log('=== 状态错配演示 ===');
// 第一次渲染:count=0,条件为false
componentHooks = [];
currentHookIndex = 0;
const [count, setCount] = useState(0); // Hook0: 存储 [0, setCount]
// if (count > 2) 为 false,跳过 useState('extra state')
const [name, setName] = useState('Alice'); // Hook1: 存储 ['Alice', setName]
console.log('第一次渲染后状态:', {
hook0: componentHooks[0][0], // 0
hook1: componentHooks[1][0] // 'Alice'
});
// 第二次渲染:count=3,条件为true
componentHooks = []; // 重新开始,但之前的状态对还在闭包中
currentHookIndex = 0;
const [count2, setCount2] = useState(0); // Hook0: 期望 0,得到 0
if (count2 > 2) { // 现在为 true!
const [extra, setExtra] = useState('extra state'); // Hook1: 期望 'Alice',但得到新状态
}
const [name2, setName2] = useState('Alice'); // Hook2: 期望新状态,但得到 'Alice'
console.log('第二次渲染后状态:', {
hook0: componentHooks[0][0], // 0
hook1: componentHooks[1][0], // 'extra state' (新状态)
hook2: componentHooks[2][0] // 'Alice' (之前的状态)
});
// 第三次渲染:count=1,条件为false
componentHooks = [];
currentHookIndex = 0;
const [count3, setCount3] = useState(0); // Hook0: 期望 0,得到 0
// if (count3 > 2) 为 false,跳过 Hook
const [name3, setName3] = useState('Alice'); // Hook1: 期望 'extra state',但得到 'Alice'
console.log('第三次渲染后状态:', {
hook0: componentHooks[0][0], // 0
hook1: componentHooks[1][0] // 'Alice' - 但系统以为这是 'extra state' 的状态!
});
// 🚨 现在 name3 实际上对应的是之前 extra 的状态位置!
// 但我们期望它对应 name 的状态!
}
在条件语句中调用 Hook 会导致:
- 状态错配:Hook 与错误的状态关联
- 状态丢失:某些状态可能被意外清除
- 不可预测的行为:组件行为变得难以理解和调试
这就是为什么 React 强制要求 Hook 必须在组件的顶层无条件调用,从而保证每次渲染时 Hook 的调用顺序完全一致。