不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook

只在最頂層使用 Hook
不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook, 確保總是在你的 React 函數(shù)的最頂層調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的 useState 和 useEffect 調(diào)用之間保持 hook 狀態(tài)的正確。(如果你對(duì)此感到好奇,我們?cè)谙旅鏁?huì)有更深入的解釋。)
我們可以在單個(gè)組件中使用多個(gè) State Hook 或 Effect Hook
function Form() {// 1. 使用變量名為 name 的 stateconst [name, setName] = useState('Mary');// 2. 使用 effect 以保存 form 操作useEffect(function persistForm() {localStorage.setItem('formData', name);});// 3. 使用變量名為 surname 的 stateconst [surname, setSurname] = useState('Poppins');// 4. 使用 effect 以更新標(biāo)題useEffect(function updateTitle() {document.title = name + ' ' + surname;});// ...}
那么 React 怎么知道哪個(gè) state 對(duì)應(yīng)哪個(gè) useState?答案是 React 靠的是 Hook 調(diào)用的順序。
let hookStates = []; // 放著此組件的所有的hooks數(shù)據(jù)let hookIndex = 0; // 代表當(dāng)前的hooks的索引function useState(initialState){// 如果有老值取老值,沒(méi)有取默認(rèn)值hookStates[hookIndex] = hookStates[hookIndex] || initialState;// 暫存索引let currentIndex = hookIndex;function setState(newState){hookStates[currentIndex] = newState;render();}return [hookStates[hookIndex++], setState];}
因?yàn)槲覀兊氖纠?,Hook 的調(diào)用順序在每次渲染中都是相同的,所以它能夠正常工作:
// ------------// 首次渲染// ------------useState('Mary') // 1. 使用 'Mary' 初始化變量名為 name 的 stateuseEffect(persistForm) // 2. 添加 effect 以保存 form 操作useState('Poppins') // 3. 使用 'Poppins' 初始化變量名為 surname 的 stateuseEffect(updateTitle) // 4. 添加 effect 以更新標(biāo)題// -------------// 二次渲染// -------------useState('Mary') // 1. 讀取變量名為 name 的 state(參數(shù)被忽略)useEffect(persistForm) // 2. 替換保存 form 的 effectuseState('Poppins') // 3. 讀取變量名為 surname 的 state(參數(shù)被忽略)useEffect(updateTitle) // 4. 替換更新標(biāo)題的 effect// ...
只要 Hook 的調(diào)用順序在多次渲染之間保持一致,React 就能正確地將內(nèi)部 state 和對(duì)應(yīng)的 Hook 進(jìn)行關(guān)聯(lián)。但如果我們將一個(gè) Hook (例如 persistForm effect) 調(diào)用放到一個(gè)條件語(yǔ)句中會(huì)發(fā)生什么呢?
// ?? 在條件語(yǔ)句中使用 Hook 違反第一條規(guī)則if (name !== '') {useEffect(function persistForm() {localStorage.setItem('formData', name);});}
在第一次渲染中 name !== '' 這個(gè)條件值為 true,所以我們會(huì)執(zhí)行這個(gè) Hook。但是下一次渲染時(shí)我們可能清空了 name,表達(dá)式值變?yōu)?false。此時(shí)的渲染會(huì)跳過(guò)該 Hook,Hook 的調(diào)用順序發(fā)生了改變:
useState('Mary') // 1. 讀取變量名為 name 的 state(參數(shù)被忽略)// useEffect(persistForm) // ?? 此 Hook 被忽略!useState('Poppins') // ?? 2 (之前為 3)。讀取變量名為 surname 的 state 失敗useEffect(updateTitle) // ?? 3 (之前為 4)。替換更新標(biāo)題的 effect 失敗
React 不知道第二個(gè) useState 的 Hook 應(yīng)該返回什么。React 會(huì)以為在該組件中第二個(gè) Hook 的調(diào)用像上次的渲染一樣,對(duì)應(yīng)的是 persistForm 的 effect,但并非如此。從這里開(kāi)始,后面的 Hook 調(diào)用都被提前執(zhí)行,導(dǎo)致 bug 的產(chǎn)生。
這就是為什么 Hook 需要在我們組件的最頂層調(diào)用。如果我們想要有條件地執(zhí)行一個(gè) effect,可以將判斷放到 Hook 的內(nèi)部:
useEffect(function persistForm() {// ??將條件判斷放置在 effect 中if (name !== '') {localStorage.setItem('formData', name);}});
不過(guò)你現(xiàn)在知道了為什么 Hook 會(huì)這樣工作,也知道了這個(gè)規(guī)則是為了避免什么問(wèn)題。
hooks 實(shí)現(xiàn)原理
import React from "react";import ReactDOM from "react-dom";// ["Mary", undefined, "Poppins", undefined]let hookStates = [];let hookIndex = 0;function useState(initialState){hookStates[hookIndex]=hookStates[hookIndex] || initialState;let currentIndex = hookIndex;function setState(newState){hookStates[currentIndex] = newState;render();}return [hookStates[hookIndex++], setState];}function useEffect(callback,dependencies){if(hookStates[hookIndex]){let lastDeps = hookStates[hookIndex];let same = dependencies.every((item,index)=>item === lastDeps[index]);if(same){hookIndex++;}else{hookStates[hookIndex++] = dependencies;setTimeout(callback);}}else{hookStates[hookIndex++] = dependencies;setTimeout(callback);}}function Counter() {const [name, setName] = useState("Mary");useEffect(function persistForm() {localStorage.setItem("formData", name);});const [surname, setSurname] = useState("Poppins");useEffect(function updateTitle() {document.title = name + ' ' + surname;});return (<><p>{name}:{surname}</p><button onClick={() => setName("張")}>姓</button><button onClick={() => setSurname("三")}>名</button></>);}function render() {hookIndex = 0;ReactDOM.render(<Counter />, document.getElementById("root"));}render();
