
文章目錄核心原因React 依靠“調(diào)用順序”來匹配 Hook底層機(jī)制單向鏈表Linked List1. 首次掛載Mount2. 重新渲染Update如果把 Hook 寫進(jìn) if 會(huì)發(fā)生什么場景演示狀態(tài)錯(cuò)位災(zāi)難第一次渲染假設(shè) isVip true第二次渲染假設(shè)用戶切換狀態(tài)isVip false正確的替代方案? 錯(cuò)誤做法條件包裹 Hook? 正確做法Hook 內(nèi)部進(jìn)行條件判斷總結(jié)React 規(guī)定Hook 絕對不能寫在條件判斷if/else、循環(huán)for/while或嵌套函數(shù)中必須始終在 React 函數(shù)組件的頂層Top Level無條件調(diào)用。這并不是 React 故意設(shè)計(jì)的限制而是由React 底層鏈表數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)和查找機(jī)制決定的。核心原因React 依靠“調(diào)用順序”來匹配 Hook在函數(shù)組件中我們可能會(huì)多次調(diào)用同一個(gè) Hook例如多次調(diào)用useStatefunction MyComponent() { const [name, setName] useState(張三); // 第 1 個(gè) Hook const [age, setAge] useState(18); // 第 2 個(gè) Hook const [role, setRole] useState(admin); // 第 3 個(gè) Hook // ... }注意到?jīng)]有我們調(diào)用useState時(shí)并沒有傳任何唯一的“Key”或標(biāo)識符給 React。那么當(dāng)組件重新渲染Re-render時(shí)React 是怎么知道第 1 個(gè)useState應(yīng)該對應(yīng)張三第 2 個(gè)對應(yīng)18的呢答案就是全靠每次渲染時(shí) Hook 的嚴(yán)格調(diào)用順序Call Order。底層機(jī)制單向鏈表Linked List在 React 底層每個(gè)組件對應(yīng)的 Fiber 節(jié)點(diǎn)內(nèi)部維護(hù)著一條Hook 單向鏈表Fiber.memoizedState ──? [ Hook 1 (name) ] ──? [ Hook 2 (age) ] ──? [ Hook 3 (role) ] ──? null1. 首次掛載Mount當(dāng)組件第一次渲染時(shí)每執(zhí)行到一個(gè) HookReact 就會(huì)創(chuàng)建一個(gè) Hook 節(jié)點(diǎn)并按順序追加到鏈表末尾。2. 重新渲染Update當(dāng)組件狀態(tài)更新重新執(zhí)行函數(shù)體時(shí)React 不會(huì)重新創(chuàng)建鏈表而是將內(nèi)部的游標(biāo)workInProgressHook移動(dòng)到鏈表頭部。順序每執(zhí)行到一個(gè) Hook游標(biāo)就向后移動(dòng)一位 (hook hook.next)從中取出上一次保存的狀態(tài)。如果把 Hook 寫進(jìn)if會(huì)發(fā)生什么假設(shè)我們違反規(guī)則將 Hook 寫在了條件語句中function BadComponent({ isVip }) { const [name, setName] useState(張三); // Hook 1 // ? 假設(shè) isVip 動(dòng)態(tài)改變 if (isVip) { const [vipLevel, setVipLevel] useState(5); // Hook 2 (條件 Hook) } const [age, setAge] useState(18); // Hook 3 }場景演示狀態(tài)錯(cuò)位災(zāi)難第一次渲染假設(shè)isVip trueReact 依次按順序構(gòu)建鏈表Hook 1──?name(張三)Hook 2──?vipLevel(5)Hook 3──?age(18)第二次渲染假設(shè)用戶切換狀態(tài)isVip false當(dāng)組件重新執(zhí)行時(shí)執(zhí)行useState(張三)游標(biāo)指向Hook 1──? 拿到張三。? 正常if (isVip)為false第 2 個(gè) Hook 被跳過接著執(zhí)行到useState(18)也就是原本的age但此時(shí) React 游標(biāo)移動(dòng)到了鏈表的下一個(gè)節(jié)點(diǎn)Hook 2React 以為這個(gè)useState對應(yīng)的是Hook 2于是把上一次vipLevel的值5賦給了age結(jié)果age的值變成了5后續(xù)所有的 Hook 索引全部錯(cuò)位引起極難排查的 Bug 甚至直接引發(fā)應(yīng)用崩潰正確的替代方案如果你需要根據(jù)條件來決定是否使用某個(gè)邏輯或計(jì)算狀態(tài)應(yīng)該把條件判斷寫在 Hook 的內(nèi)部/下方而不是把 Hook 寫在條件判斷里? 錯(cuò)誤做法條件包裹 Hookif (isLoggedIn) { useEffect(() { fetchUserData(); }, []); }? 正確做法Hook 內(nèi)部進(jìn)行條件判斷useEffect(() { // Hook 依然無條件執(zhí)行但內(nèi)部邏輯根據(jù)條件退出 if (!isLoggedIn) return; fetchUserData(); }, [isLoggedIn]);總結(jié)本質(zhì)原因React 沒有給 Hook 提供唯一標(biāo)識符Key內(nèi)部通過單向鏈表和調(diào)用順序Index來對齊狀態(tài)。后果如果在if/for中使用 Hook條件切換會(huì)導(dǎo)致 Hook 的調(diào)用次數(shù)與鏈表長度錯(cuò)位造成狀態(tài)讀取混亂。保障機(jī)制React 官方提供的 ESLint 插件eslint-plugin-react-hooks會(huì)在編譯期自動(dòng)檢測并禁止這種違規(guī)寫法rules-of-hooks。 感謝閱讀想了解更多 我的博客網(wǎng)站 | 記錄思考分享干貨 我的個(gè)人主頁 | 關(guān)于我、開源項(xiàng)目