理解 React Hooks 心智模型:必须按顺序、不能在条件语句中调用的规则
前言
自从 React 推出 hooks 的 API 后,相信大家对新 API 都很喜欢,但是它对你如何使用它会有一些奇怪的限制。比如,React 官网介绍了 Hooks 的这样一个限制:
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的
useState
和useEffect
调用之间保持 hook 状态的正确。
useState Hook 的工作原理
这个限制并不是 React 团队凭空造出来的,的确是由于 React Hooks 的实现设计而不得已为之。
为了让大家有一个更清晰的思维模型,我将用数组来模拟useState
的简单实现。
首先让我们通过一个例子来看看 hook 是如何工作的。
我们首先从一个组件开始:
function RenderFunctionComponent() { const [firstName, setFirstName] = useState("Rudi"); const [lastName, setLastName] = useState("Yardley"); return ( <Button onClick={() => setFirstName("Fred")}>Fred</Button> ); }
useState hook 背后的思想是,你可以使用 hook 函数返回的数组的第二个数组项作为 setter 函数,并且该 setter 将控制由 hook 管理的状态。
具体实现
1)初始化
创建两个空数组:setters
和 state
将 cursor 设置为 0
2)第一次渲染
首次运行组件函数。
每次useState()调用,在第一次运行时,都会将一个 setter 函数推送到 setters 数组上,然后将一些状态推送到 state 数组上。
3)后续渲染
每次后续渲染都会重置 cursor,并且仅从每个数组中读取这些值。
4)事件处理
每个 setter 都有对其 cursor 的引用,因此通过触发对 setter 的调用,setter 它将更改状态数组中该位置的状态值。
简单代码实现
下面通过一段简单的代码示例来演示该实现。
注意:这并不是 React 的底层实现,但对于我们理解 react hook 的心智模型非常有帮助。
const state = []; const setters = []; let cursor = 0; function createSetter(cursor) { return function setterWithCursor(newVal) { state[cursor] = newVal; }; } export function useState(initVal) { if (state[cursor] === undefined && setters[cursor] === undefined) { state.push(initVal); setters.push(createSetter(cursor)); } const setter = setters[cursor]; const value = state[cursor]; cursor++; return [value, setter]; } function RenderFunctionComponent() { const [firstName, setFirstName] = useState('Rudi'); // cursor: 0 const [lastName, setLastName] = useState('Yardley'); // cursor: 1 return ( <div> <button onClick={() => setFirstName('Richard')}>Richard</button> <button onClick={() => setLastName('Fred')}>Fred</button> </div> ); } function MyComponent() { cursor = 0; // resetting the cursor return <RenderFunctionComponent />; // render } console.log(state); // Pre-render: [] MyComponent(); console.log(state); // First-render: ['Rudi', 'Yardley'] MyComponent(); console.log(state); // Subsequent-render: ['Rudi', 'Yardley'] // click the 'Richard' button console.log(state); // After-click: ['Richard', 'Yardley']
为什么顺序很重要
现在,如果我们根据某些外部因素甚至组件状态更改渲染周期的钩子顺序会发生什么?
让我们做 React 团队说你不应该做的事情:
let firstRender = true; function RenderFunctionComponent() { let initName; if(firstRender){ [initName] = useState("Rudi"); firstRender = false; } const [firstName, setFirstName] = useState(initName); const [lastName, setLastName] = useState("Yardley"); return ( <Button onClick={() => setFirstName("Fred")}>Fred</Button> ); }
这里我们有useState
的一个条件调用。让我们看看这对系统造成的破坏。
Bad Component 第一次渲染
我们的实例变量firstName
和lastName
包含正确的数据,但让我们看看第二次渲染会发生什么:
Bad Component 第二次渲染
现在,firstName
和lastName
发生了错位,我们的状态存储变得不一致了。这就是为什么保持正确顺序的重要性。
总结:
通过对 useState 的简单实现来理解 react hooks 的幕后实现逻辑。考虑将状态作为一组数组存在的模型,那么我们不该违反其对应的使用规则。