为了确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
我们可以在单个组件中使用多个 State Hook 或 Effect Hook:
function Form() {
// 1. 使用变量名为 name 的 state
const [name, setName] = useState('Mary');
// 2. 使用 effect 以保存 form 操作
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. 使用变量名为 surname 的 state
const [surname, setSurname] = useState('Poppins');
// 4. 使用 effect 以更新标题
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。
let hookStates = []; // 放着此组件的所有的hooks数据
let hookIndex = 0; // 代表当前的hooks的索引
function useState(initialState){
// 如果有老值取老值,没有取默认值
hookStates[hookIndex] = hookStates[hookIndex] || initialState;
// 暂存索引
let currentIndex = hookIndex;
//setState方法,将当前值置为新值
function setState(newState){
hookStates[currentIndex] = newState;
//置完新值,立刻触发渲染
render();
}
return [hookStates[hookIndex++], setState];
}
因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// useState - 不再使用传入的默认值,而是返回上次渲染时存储的状态值。
// useEffect 首先运行之前 useEffect 的清理函数(如果有的话),然后再运行新的副作用函数。
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(Mary参数被忽略)
useEffect(persistForm) // 2. 清除并替换上一次 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(Poppins参数被忽略)
useEffect(updateTitle) // 4. 清除并替换上一次更新标题的 effect
// ...
useEffect
Hook有什么作用?如何理解 替换与清除?
告诉 React 需要在完成DOM更新之后(渲染之后)执行一些“副作用”(如数据获取、手动更改DOM、设置订阅或者清除上一次的副作用等)。
useEffect(persistForm)
在首次渲染时添加了一个副作用,即 persistForm
函数。然后,在随后的组件更新(第二次渲染)中,相同的 useEffect(persistForm)
调用会首先清除前一次的副作用(如果 persistForm
返回了一个清除函数的话),然后再执行新的 persistForm
副作用。
这样确保了副作用是最新的,并且在多次渲染之间不会有冲突。这也是为什么我们说第二次和随后的渲染是“替换”前一次的副作用。
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?
比如:
// ???? 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了 name,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // ???? 此 Hook 被忽略!
useState('Poppins') // ???? 2(之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // ???? 3 (之前为 4)。替换更新标题的 effect 失败
第一次调用 useState('Mary')
,此时 hookIndex=0
,状态被存储在 hookStates[0]
。
然后我们跳过 useEffect(persistForm)
。
接下来的 useState('Poppins')
会使用 hookIndex=1
,但在上一次渲染中,它使用了 hookIndex=2
。这会导致 surname 的状态从错误的位置获取。
以此类推,后续的 useEffect(updateTitle)
也会使用错误的索引。