React Hooks原理:为什么不能写在if里?揭开Hook的“魔法”面纱
前言
Hooks刚出的时候,大家都觉得是“黑魔法”:一个函数组件,居然能记住自己的状态?还能模拟生命周期?很多人用了很久,却不知道原理。导致遇到奇怪的问题(比如无限循环、状态不更新)时,只能靠试。
今天我们不背源码,用最简单的代码模拟React Hooks的核心机制。学完你不仅能解释“为什么不能在条件语句里调用Hook”,还能自己手写一个迷你useState。
一、函数组件为啥需要Hooks?
类组件有this.state和生命周期,但函数组件每次渲染都会重新执行,里面的变量都会重新创建。那怎么保存状态?React用了闭包+链表。
每个函数组件对应一个“虚拟节点”(Fiber节点),它上面有个memoizedState属性,用来保存该组件的Hooks链表。
二、模拟React Hooks:手写迷你useState
我们先不管React的实现细节,用纯JS模拟一个最简单的useState。
lethooks=null;// 当前组件的hooks链表letcurrentHook=0;// 当前正在执行的hook索引functionuseState(initialValue){// 如果是第一次渲染,初始化这个hook的值if(!hooks[currentHook]){hooks[currentHook]={state:initialValue};}consthook=hooks[currentHook];constsetState=(newValue)=>{hook.state=newValue;scheduleRender();// 触发重新渲染(伪代码)};currentHook++;return[hook.state,setState];}functionrender(component){hooks=[];// 重置hooks链表currentHook=0;// 重置索引constvdom=component();// 执行组件,收集hooks// 渲染vdom...}关键点:
hooks数组按调用顺序存储每个useState的状态。- 每次渲染,
currentHook重置为0,依次取出对应的状态。 - 所以必须保证每次渲染时,Hook的调用顺序和数量完全一致。这就是不能在
if或循环里调用的根本原因。
三、为什么顺序必须不变?
假设第一次渲染时,你在if里调用了useState,第二次渲染时条件不成立,那个Hook被跳过了。那么后续Hook的对应关系就会错位:本来应该取第二个状态,结果取了第三个的。React就会报错。
// 错误示例functionMyComponent({flag}){if(flag){const[a,setA]=useState(1);// 第一次有,第二次没有}const[b,setB]=useState(2);// 第一次是第二个hook,第二次变成了第一个}React团队之所以这样设计,是为了在保证性能的同时简化实现。用数组/链表存储,比用Map key查找快得多。
四、多个Hook是怎么串联的?
React实际用的是单向链表,每个Hook节点有next指针指向下一个。这样即使组件不渲染,链表也保留在Fiber节点上。
// 简化的链表结构consthook={memoizedState:null,// 当前状态next:null,// 下一个hook// 还有queue等用于更新的字段};每次渲染,React根据上次的链表和本次调用的顺序,把新状态赋给对应的Hook。
五、useEffect的原理:等渲染完再执行
useEffect的回调不会阻塞浏览器绘制,它是在渲染提交到屏幕之后异步执行的。它的存储也类似,但多了清除函数的管理。
functionuseEffect(callback,deps){consthook=hooks[currentHook];constprevDeps=hook?.deps;consthasChanged=!prevDeps||deps.some((dep,i)=>dep!==prevDeps[i]);if(hasChanged){// 将callback放到待执行队列,等渲染完成后执行scheduleEffect(callback);}hook.deps=deps;currentHook++;}六、为什么不能在循环里调用Hook?
跟if同理:循环次数变了,Hook的顺序就变了。即使你保证循环次数不变,也没法阻止以后的维护者改代码。所以React直接禁止这种写法。
七、useCallback和useMemo本质是缓存
它们也存储在Hook链表里,只是memoizedState里存的是缓存的值和依赖。
functionuseMemo(factory,deps){consthook=hooks[currentHook];constprevDeps=hook?.deps;consthasChanged=!prevDeps||deps.some((d,i)=>d!==prevDeps[i]);if(hasChanged){hook.value=factory();hook.deps=deps;}currentHook++;returnhook.value;}八、自定义Hook为什么没有特殊待遇?
自定义Hook只是调用了内置Hook的普通函数,它不会新增链表节点,只是把调用的内置Hook顺序归入组件的链表。所以自定义Hook的规则也遵循“只在顶层调用”。
九、总结:Hooks的“交通规则”
- Hooks用链表存储,顺序就是调用顺序。
- 每次渲染必须保持完全相同的调用顺序和数量。
- 所以禁止在条件、循环、嵌套函数里调用Hook。
useState返回的setter之所以能拿到最新值,是因为闭包引用了Hook对象,而Hook对象上的状态会被更新。
理解了这个原理,你再也不会害怕Hooks的诡异报错。下次同事问“为什么不能if里写useState”,你可以拍拍他肩膀:“因为React用数组存状态,你跳过一个,后面的全对不上了。”
