React19源码系列之Hooks(useId)
useId的介绍
https://zh-hans.react.dev/reference/react/useId
useId
是 React 18 引入的一个新 Hook,主要用于生成全局唯一的 ID。在开发中,我们经常需要为元素(如表单元素、模态框等)生成唯一 ID,以便在 JavaScript 中进行操作或在 CSS 中进行样式绑定。使用 useId 可以避免手动管理 ID 带来的问题,比如在服务器端渲染(SSR)时客户端和服务器端生成的 ID 不一致。
useId的使用
例子:基本用法
import React, { useId } from 'react';
function App() {
const id = useId();
return (
<div>
<label htmlFor={id}>用户名:</label>
<input type="text" id={id} />
</div>
);
}
export default App;
例子:处理多个ID
import React, { useId } from 'react';
function App() {
const baseId = useId();
const nameId = `${baseId}-name`;
const emailId = `${baseId}-email`;
return (
<form>
<label htmlFor={nameId}>姓名:</label>
<input type="text" id={nameId} />
<br />
<label htmlFor={emailId}>邮箱:</label>
<input type="email" id={emailId} />
</form>
);
}
export default App;
例子:在组件树中使用
useId
在组件树中使用时,每个组件实例都会生成不同的 ID。这意味着即使在嵌套组件中多次使用 useId
,也不会产生冲突
import React, { useId } from 'react';
function InputWithLabel({ labelText }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{labelText}</label>
<input type="text" id={id} />
</div>
);
}
function App() {
return (
<div>
<InputWithLabel labelText="用户名" />
<InputWithLabel labelText="密码" />
</div>
);
}
export default App;
hook 初始化入口
hook是函数组件中的钩子函数。在函数组件渲染的过程中会调用renderWithHooks
函数。
renderWithHooks
函数参数含义:
current
:旧的 Fiber 节点,如果是首次渲染则为 null。workInProgress
:当前正在处理的新 Fiber 节点。Component
:要渲染的函数式组件,它接收 props 和 secondArg 作为参数,并返回 JSX 元素。props
:传递给组件的属性。secondArg
:额外的参数。nextRenderLanes
:下一次渲染的优先级车道。
function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 确定当前渲染的优先级。
renderLanes = nextRenderLanes;
// 将 currentlyRenderingFiber 指向 workInProgress,表示当前正在渲染这个 Fiber 节点。
currentlyRenderingFiber = workInProgress;
// 重置信息
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
//钩子调度器,区分是初始化还是更新
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
//调用函数组件
let children = Component(props, secondArg);
// 调用 finishRenderingHooks 函数,进行一些渲染完成后的清理和处理工作,例如处理副作用钩子的执行等。
finishRenderingHooks(current, workInProgress, Component);
return children;
}
// renderLanes:表示当前渲染的优先级车道,初始值为 NoLanes(通常为 0),用于标识渲染任务的优先级。
let renderLanes: Lanes = NoLanes;//默认0
// 指向当前正在渲染的 Fiber 节点,初始为 null。Fiber 是 React 中的一种数据结构,用于表示组件树中的每个节点。
let currentlyRenderingFiber: Fiber = (null: any);
// 当前的钩子
let currentHook: Hook | null = null;
//正在处理的钩子
let workInProgressHook: Hook | null = null;
所以初始化用HooksDispatcherOnMount
,更新使用HooksDispatcherOnUpdate
HooksDispatcherOnMount
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
HooksDispatcherOnUpdate
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
use,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
};
useId初始化
function useId(): string {
//resolveDispatcher 是 React 内部的一个函数,它的作用是获取当前的调度器(dispatcher)
const dispatcher = resolveDispatcher();
// dispatcher.useId() 调用了调度器对象的 useId 方法。这个方法会生成一个全局唯一的 ID 字符串,并将其返回。
return dispatcher.useId();
}
resolveDispatcher
function resolveDispatcher() {
// 从 ReactSharedInternals 对象中获取名为 H 的属性,并将其赋值给变量 dispatcher。ReactSharedInternals 是 React 内部使用的一个共享对象,它包含了一些 React 运行时的关键信息和工具。H 属性在这里代表着当前 React 环境下的调度器实例。
const dispatcher = ReactSharedInternals.H;
// 将获取到的调度器实例 dispatcher 返回给调用者。这样,调用 resolveDispatcher 函数的代码就可以使用这个调度器来执行各种调度相关的操作,比如调用调度器的 useState 方法来处理状态管理。
return dispatcher;
}
mountId
mountId
函数是 React 中 useId
Hook 在挂载阶段(组件首次渲染)用于生成全局唯一 ID 的核心实现。该函数会根据当前是服务端渲染(SSR
)还是客户端渲染的不同情况,生成不同格式的唯一 ID,并将其存储在 Fiber 节点对应的 hook
对象的 memoizedState
属性上,最后返回这个唯一 ID。
function mountId(): string {
//创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();
//getWorkInProgressRoot() 方法获取当前的 FiberRoot 对象
const root = ((getWorkInProgressRoot(): any): FiberRoot);
//从 FiberRoot 对象 上获取id前缀
const identifierPrefix = root.identifierPrefix;
let id;
// 调用 getIsHydrating() 方法判断是服务端渲染还是客户端渲染
if (getIsHydrating()) {
//服务端渲染注水(hydrate)阶段生成的唯一 id,以冒号开头,并以冒号结尾,使用大写字母 R 标识该id是服务端渲染生成的id
//获取组件树的id
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
id = ':' + identifierPrefix + 'R' + treeId;
// localIdCounter 变量记录组件中 useId 的执行次数
const localId = localIdCounter++;
if (localId > 0) {
id += 'H' + localId.toString(32);
}
id += ':';
} else {
//客户端渲染生成的唯一 id,以冒号开头,并以冒号结尾,使用小写字母 r 标识该id 是客户端渲染生成的id
//全局变量 globalClientIdCounter 记录 useId hook 在组件中的调用次数
const globalClientId = globalClientIdCounter++;// globalClientIdCounter初始值为0
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
}
// 将生成的唯一 id 存储到 hook 对象的 memoizedState 属性上
hook.memoizedState = id;
return id;
}
以下代码可以看到useId
的返回值的格式
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
// ':r0:'
如何在挂载的时候加入配置项identifierPrefix
ReactDOM.createRoot(document.getElementById('box'),{
identifierPrefix: 'testyoyo'
});
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
//:testyoyor0:
updateId
当函数组件刷新时,重新构建hook
链表时,遇到useId
会执行以下代码
function updateId(): string {
// 获取当前的 workInProgressHook
const hook = updateWorkInProgressHook();
// 从当前的 workInProgressHook 上获取 useId 生成的 id,因此即使组件重新渲染,id 也不会变化
const id: string = hook.memoizedState;
return id;
}
工具函数 mountWorkInProgressHook
mountWorkInProgressHook
函数主要用于在 React 的渲染过程中,为当前正在处理的 Fiber 节点(currentlyRenderingFiber
)挂载一个新的 Hook。
Hook 是 React 中用于在函数组件中使用状态和副作用的机制,这个函数负责初始化 Hook 对象,并将其添加到当前 Fiber 节点的 Hook 链表中。
function mountWorkInProgressHook(): Hook {
// 初始化hook对象
const hook: Hook = {
memoizedState: null,//用于存储当前 Hook 的状态值,在使用 useState 或 useReducer 等钩子时会更新这个值。
baseState: null,// 表示状态的基础值,在处理更新队列时会用到。
baseQueue: null,// 存储尚未处理的更新队列,通常与 baseState 配合使用。
queue: null,// 存储当前 Hook 的更新队列,用于存储状态更新的操作。
next: null,// 用于将多个 Hook 对象连接成一个链表,指向下一个 Hook 对象。
};
// 如果 workInProgressHook 为 null,说明这是当前 Fiber 节点的第一个 Hook。
if (workInProgressHook === null) {
// 将 currentlyRenderingFiber 的 memoizedState 属性设置为新创建的 Hook 对象,并将 workInProgressHook 也指向这个 Hook 对象。这样,currentlyRenderingFiber 的 memoizedState 就成为了 Hook 链表的头节点。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 如果 workInProgressHook 不为 null,说明当前 Fiber 节点已经有其他 Hook 存在。此时,将新创建的 Hook 对象添加到 Hook 链表的末尾。
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前正在处理的 Hook 对象
return workInProgressHook;
}
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};