当前位置: 首页 > news >正文

10_从 React Hooks 本质看 useState

一、Hooks 的本质

Hooks 是挂在 Fiber 上的一条“有序链表”,通过“调用顺序”来定位状态

每个函数组件对应一个 Fiber:

type Fiber = {memoizedState: Hook | null; // Hook 链表头
}

对于一个 Hook,有三种类型的 dispatcher(可以认为是操作策略):

/* 函数组件初始化用的 hooks */
// 初始化信息挂载到 fiber 上
const HooksDispatcherOnMount: Dispatcher = {...useCallback: mountCallback,useEffect: mountEffect,useMemo: mountMemo,useReducer: mountReducer,useRef: mountRef,useState: mountState,...
};/* 函数组件更新用的 hooks */
// 组件更新执行对应的方法,更新 fiber 信息
const HooksDispatcherOnUpdate: Dispatcher = {...useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useMemo: updateMemo,useReducer: updateReducer,useRef: updateRef,useState: updateState,...
};/* 当 hooks 不是函数组件内部调用或者嵌套 hooks 等“非正确使用”情况,调用这些报错相关的 dispatcher */
const ContextOnlyDispatcher: Dispatcher = {...useCallback: throwInvalidHookError,useContext: throwInvalidHookError,useEffect: throwInvalidHookError,useMemo: throwInvalidHookError,useReducer: throwInvalidHookError,useRef: throwInvalidHookError,useState: throwInvalidHookError,...
};

二、Hook 的数据结构

type Hook = {memoizedState: any; // 当前值baseState: any;queue: UpdateQueue | null;next: Hook | null;
}

注意:这里要和 FiberNode 的 memoizedState 区分开:

  • FiberNode.memoizedState:保存的是 Hook 链表里面的第一个链表
  • hook.memoizedState:某个 Hook 自身的数据
Fiber.memoizedState↓
[useState] → [useEffect] → [useMemo] → null

完全依赖调用顺序!

不同的 hook,memoizedState 所存储的内容不同:

  • useState:对于 const [state, updateState] = useState(initialState),memoizedState 保存的是 state 的值
  • useReducer:对于 const [state, dispatch] = useReducer(reducer, { } ),memoizedState 保存的是 state 的值
  • useEffect:对于 useEffect( callback, [...deps] ),memoizedState 保存的是 callback、[...deps] 等数据
  • useRef:对于 useRef(initialValue),memoizedState 保存的是 { current: initialValue}
  • useMemo:对于 useMemo( callback, [...deps] ),memoizedState 保存的是 [callback( )、[...deps]] 数据
  • useCallback:对于 u seCallback( callback, [...deps] ),memoizedState 保存的是 [callback、[...deps]] 数据
  • useContext:不需要 memoizedState 保存自身数据

三、执行流程(mount 阶段)

1️⃣ render 开始

function Component() {const [count, setCount] = useState(0);
}
// render 开始先执行
export function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {renderLanes = nextRenderLanes;currentlyRenderingFiber = workInProgress;// 每一次执行函数组件之前,先清空 FiberNode 状态 (用于存放 hooks 列表)workInProgress.memoizedState = null;// 清空更新队列(用于存放 effect 列表)workInProgress.updateQueue = null;// ...// 根据不同的组件状态初始化不同的 dispatcher 对象和上下文ReactCurrentDispatcher.current =current === null || current.memoizedState === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;// 执行函数组件,所有的 hooks 将依次执行let children = Component(props, secondArg);// ...// 兜底finishRenderingHooks(current, workInProgress);return children;
}function finishRenderingHooks(current, workInProgress) {// 防止 hooks 在不合规的情况下调用,如果调用直接报错ReactCurrentDispatcher.current = ContextOnlyDispatcher;// ...
}

2️⃣ mountState 做了什么

如果组件是挂载阶段:

function mountWorkInProgressHook() {const hook = {memoizedState: null,  // Hook 自身的状态baseState: null,baseQueue: null,queue: null, // hook 自身队列next: null, // next 指向下一个 hook};// 判断当前的 hook 是否是链表的第一个if (workInProgressHook === null) {// 如果当前组件的 Hook 链表为空,那么就将刚刚新建的 Hook 作为 Hook 链表的第一个节点(头结点) ​currentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {// 果当前组件的 Hook 链表不为空,那么就将刚刚新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}function mountStateImpl(initialState) {// 获取 hook 对象const hook = mountWorkInProgressHook();//...// 初始化 memoizedState hook.memoizedState = hook.baseState = initialState;const queue: UpdateQueue = {pending: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: basicStateReducer, // useState 内置的 reducerlastRenderedState: (initialState: any),};// 初始化 queuehook.queue = queue;return hook;
}function mountState(initialState) {// 获取 hook 对象const hook = mountStateImpl(initialState);const queue = hook.queue;const dispatch = (dispatchSetState.bind(null,currentlyRenderingFiber,queue,));// 初始化 dispatch (dispatch 就是用来修改状态的方法)queue.dispatch = dispatch;// 返回 [当前状态, dispatch函数]return [hook.memoizedState, dispatch];
}

其实 useReducer 和 useState 非常像,在源码层面:

  1. mount 阶段:mountState 和 mountReducer 的大体流程是一样的。但是有一个区别,mountState 的 queue 里面的 lastRenderedReducer 对应的是 basicStateReducer,而 mountReducer 的 queue 里面的 lastRenderedReducer 对应的是​开发者自己传入的 reducer​,这里说明了一个问题,useState 的本质就是 useReducer 的一个简化版,只不过在 useState 内部,会有一个内置的 reducer
  2. update 阶段:在 update 阶段,updateState 内部直接调用的就是 updateReducer,传入的 reducer 仍然是 basicStateReducer。
function mountReducer(reducer, initialArg, init) {// 创建 hook 对象const hook = mountWorkInProgressHook();let initialState;// 如果有 init 初始化函数,就执行该函数,并将执行的结果赋值给 initialStateif (init !== undefined) {initialState = init(initialArg);} else {initialState = initialArg;}// 赋值给 hook 对象的 memoizedStatehook.memoizedState = hook.baseState = initialState;const queue = {pending: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: reducer, // 手动传入的 reducerlastRenderedState: initialState,};hook.queue = queue;const dispatch = (queue.dispatch = dispatchReducerAction.bind(null,currentlyRenderingFiber,queue));return [hook.memoizedState, dispatch];
}

3️⃣ 构建 Hook 链表

第一次 render:Fiber.memoizedState → Hook1 → Hook2 → Hook3

举个例子~

该示例来源:渡一教育。

function App() {const [number, setNumber] = React.useState(0); // 第一个hookconst [num, setNum] = React.useState(1); // 第二个hookconst dom = React.useRef(null); // 第三个hookReact.useEffect(() => {// 第四个hookconsole.log(dom.current);}, []);return (<div ref={dom}><div onClick={() => setNumber(number + 1)}> {number} </div><div onClick={() => setNum(num + 1)}> {num}</div></div>);
}

四、更新阶段(update)

不再创建 Hook,而是“复用”

function updateWorkInProgressHook(){let nextCurrentHook: null | Hook;if (currentHook === null) {// 从 alternate 上获取到 fiber 对象const current = currentlyRenderingFiber.alternate;// 获取第一个 hookif (current !== null) {nextCurrentHook = current.memoizedState;} else {nextCurrentHook = null;}} else {// 获取下一次 hooknextCurrentHook = currentHook.next;}// workInProgressHook 会指向下一个要工作的 hooklet nextWorkInProgressHook: null | Hook;if (workInProgressHook === null) {nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;} else {nextWorkInProgressHook = workInProgressHook.next;}if (nextWorkInProgressHook !== null) {// 已经存在,直接复用workInProgressHook = nextWorkInProgressHook;nextWorkInProgressHook = workInProgressHook.next;currentHook = nextCurrentHook;} else {// Clone from the current hook.// 如果 nextWorkInProgressHook 不为 null,那么就会复用之前的 hook// 划重点!!!// 更新的过程中,如果通过条件语句增加或者删除了 hook,复用的时候就会产生当前 hook 的顺序和之前 hook 的顺序不一致的问题if (nextCurrentHook === null) {const currentFiber = currentlyRenderingFiber.alternate;if (currentFiber === null) {// This is the initial render. This branch is reached when the component// suspends, resumes, then renders an additional hook.// Should never be reached because we should switch to the mount dispatcher first.throw new Error('Update hook called on initial render. This is likely a bug in React. Please file an issue.',);} else {// This is an update. We should always have a current hook.throw new Error('Rendered more hooks than during the previous render.');}}currentHook = nextCurrentHook;const newHook: Hook = {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null,};if (workInProgressHook === null) {// This is the first hook in the list.currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;} else {// Append to the end of the list.workInProgressHook = workInProgressHook.next = newHook;}}return workInProgressHook;
}function updateReducer() {const hook = updateWorkInProgressHook();return updateReducerImpl(hook, ((currentHook)), reducer);
}function updateState<S>(initialState) {return updateReducer(basicStateReducer, initialState);
}

接着上面的示例~

示例来源:渡一教育。

function App({ showNumber }) {let number, setNumbershowNumber && ([ number,setNumber ] = React.useState(0)) // 第一个hooksconst [num, setNum] = React.useState(1); // 第二个hookconst dom = React.useRef(null); // 第三个hookReact.useEffect(() => {// 第四个hookconsole.log(dom.current);}, []);return (<div ref={dom}><div onClick={() => setNumber(number + 1)}> {number} </div><div onClick={() => setNum(num + 1)}> {num}</div></div>);
}

假设第一次父组件传递过来的 showNumber 为 true,此时就会渲染第一个 hook;第二次渲染的时候,假设父组件传递过来的是 false,那么第一个 hook 就不会执行,那么逻辑就会变得:

第一次:useState -> useState

第二次:useState -> useRef

体现在我们开发者眼中就是报错。

五、setState 到底做了什么?

dispatch 流程

function dispatchSetState(action) {const update = {action,next: null};enqueueUpdate(queue, update);scheduleUpdateOnFiber(fiber);
}

UpdateQueue 结构

hook.queue↓
update1 → update2 → update3(环形链表)

执行更新

function processUpdateQueue(queue) {let state = baseState;queue.forEach(update => {state = reducer(state, update.action);});return state;
}

六、调度机制(Hooks 如何触发更新)

scheduleUpdateOnFiber(fiber)
setState↓
scheduleUpdate↓
标记 lane(优先级)↓
render(可中断)↓
commit(不可中断)

Hooks 如何保证并发下的 hooks 行为正确?

关键:

  • 每次 render 都重新走一遍 Hook 链
  • 不依赖“执行次数”,只依赖“顺序”
http://www.jsqmd.com/news/745723/

相关文章:

  • Unlock Music:浏览器端免费解密加密音乐文件的完整实践指南
  • 如何用DS4Windows实现PS手柄在Windows上的完美游戏体验:终极配置指南
  • Java 25 ZGC 2.0低延迟调优实战(生产环境0.8ms P99停顿实录)
  • 中小团队如何利用Taotoken统一管理多个AI模型的API调用成本
  • 5分钟快速完成Axure RP免费中文汉化:终极完整指南
  • League Akari:重新定义英雄联盟的游戏助手体验
  • Depth-Anything-V2:如何在5分钟内实现高精度单目深度估计
  • 如何在Windows系统上快速部署iperf3网络性能测试工具:终极实战指南
  • Allegro PCB布线小技巧:移动元件时,如何让导线乖乖跟着走?(Options选项详解)
  • 使用 TaoToken CLI 工具一键配置开发环境与写入密钥
  • ROS2参数管理避坑指南:为什么你的RCLPY节点没收到参数变更通知?
  • 如何在Windows上使用OpenSpeedy开源游戏变速工具:3分钟快速上手终极指南
  • 别再死记硬背CNN结构了!用PyTorch手把手搭建一个图像分类器(附完整代码)
  • 跨平台漫画阅读器JHenTai:5大核心功能深度解析与使用指南
  • League Akari终极指南:英雄联盟智能游戏管家完整配置与高效使用方案
  • 告别视频下载烦恼:bilibili-parse让你的B站视频获取如此简单
  • Anthropic推出Claude Security公开测试版:AI驱动代码漏洞扫描与自动修复工具
  • Battery Toolkit:为Apple Silicon Mac延长50%电池寿命的开源电源管理解决方案
  • 别再死记硬背了!用Protege手把手教你构建知识图谱的‘骨架’(本体建模实战)
  • 局域网内实现电脑间快速传输超大文件并支持断点续传的三种工具
  • 别再手动敲公式了!用IguanaTex插件,5分钟搞定PowerPoint里的LaTeX数学公式
  • 保姆级教程:在Ubuntu22.04上为ROS2 Humble搞定CH340串口驱动与权限问题
  • PPTist终极指南:3分钟掌握免费在线PPT制作,告别PowerPoint依赖
  • 告别数据灾难:Linux下flash_erase命令的‘锁’与‘备份’实操指南
  • 终极免费OCR解决方案:如何用Umi-OCR离线批量识别图片文字
  • Windows上直接安装Android应用的终极解决方案:APK Installer使用全指南
  • 163MusicLyrics:一键获取全网音乐歌词的终极解决方案
  • 5个理由告诉你为什么TouchGAL是Galgame爱好者的终极选择
  • 使用curl命令在无图形界面虚拟机中测试Taotoken API连通性
  • 百度文库助手:三步解锁文档自由,让你的学习效率翻倍