likes
comments
collection
share

React Hooks 源码解读之 useId

作者站长头像
站长
· 阅读数 13

react 版本:v18.3.0

1、Hook 入口

React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:

  • 挂载阶段:

    useId = ReactCurrentDispatcher.current.useId = HooksDispatcherOnMount.useId = mountId;

  • 更新阶段:

    useId = ReactCurrentDispatcher.current.useId = HooksDispatcherOnUpdate.useId = updateId;

因此,组件在挂载阶段,执行 useId,其实执行的是 mountId,而在更新阶段,则执行的是updateId 。

2、挂载阶段

组件在挂载阶段,执行 useId,实际上执行的是 mountId,下面我们来看看 mountId 的实现:

2.1 mountId

// packages/react-reconciler/src/ReactFiberHooks.js

function mountId(): string {
  // 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
  const hook = mountWorkInProgressHook();

  // 获取 FiberRoot 对象 
  const root = ((getWorkInProgressRoot(): any): FiberRoot);
  // TODO: In Fizz, id generation is specific to each server config. Maybe we
  // should do this in Fiber, too? Deferring this decision for now because
  // there's no other place to store the prefix except for an internal field on
  // the public createRoot object, which the fiber tree does not currently have
  // a reference to.
  const identifierPrefix = root.identifierPrefix;

  let id;
  if (getIsHydrating()) {

    // 服务端渲染注水(hydrate)阶段生成的唯一 id,以冒号开头,并以冒号结尾,使用大写字母 R 标识该id是服务端渲染生成的id

    // 获取组件树的id
    const treeId = getTreeId();

    // 拼接 id,以冒号开头,并以冒号结尾
    // Use a captial R prefix for server-generated ids.
    id = ':' + identifierPrefix + 'R' + treeId;

    // Unless this is the first id at this level, append a number at the end
    // that represents the position of this useId hook among all the useId
    // hooks for this fiber.
    
    // localIdCounter 变量记录组件中 useId 的执行次数
    const localId = localIdCounter++;
    if (localId > 0) {
      id += 'H' + localId.toString(32);
    }

    id += ':';
  } else {

    // 客户端渲染生成的唯一 id,以冒号开头,并以冒号结尾,使用小写字母 r 标识该id 是客户端渲染生成的id
    
    // Use a lowercase r prefix for client-generated ids.

    // 全局变量 globalClientIdCounter 记录 useId hook 在组件中的调用次数
    const globalClientId = globalClientIdCounter++;
     // 拼接 id,以冒号开头,并以冒号结尾
    id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
  }

  // 将生成的唯一 id 存储到 hook 对象的 memoizedState 属性上
  hook.memoizedState = id;
  return id;
}

组件在挂载时,执行 useId,首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中:

// 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();

接着调用 getWorkInProgressRoot() 方法获取当前的 FiberRoot 对象,从 FiberRoot 对象 上获取id前缀:

// 获取 FiberRoot 对象 
const root = ((getWorkInProgressRoot(): any): FiberRoot);

const identifierPrefix = root.identifierPrefix;

然后调用 getIsHydrating() 方法判断是服务端渲染还是客户端渲染,根据不同的渲染方式执行不同的逻辑生成id:

if (getIsHydrating()) {

  // 服务端渲染注水(hydrate)阶段生成的唯一 id,以冒号开头,并以冒号结尾,使用大写字母 R 标识该id是服务端渲染生成的id

  // 获取组件树的id
  const treeId = getTreeId();

  // 拼接 id,以冒号开头,并以冒号结尾
  // Use a captial R prefix for server-generated ids.
  id = ':' + identifierPrefix + 'R' + treeId;

  // Unless this is the first id at this level, append a number at the end
  // that represents the position of this useId hook among all the useId
  // hooks for this fiber.
  
  // localIdCounter 变量记录组件中 useId 的执行次数
  const localId = localIdCounter++;
  if (localId > 0) {
    id += 'H' + localId.toString(32);
  }

  id += ':';
} else {

  // 客户端渲染生成的唯一 id,以冒号开头,并以冒号结尾,使用小写字母 r 标识该id 是客户端渲染生成的id
  
  // Use a lowercase r prefix for client-generated ids.

  // 全局变量 globalClientIdCounter 记录 useId hook 在组件中的调用次数
  const globalClientId = globalClientIdCounter++;
   // 拼接 id,以冒号开头,并以冒号结尾
  id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
}

从上面的代码可以看到,无论是服务端渲染还是客户端渲染,生成的id的第一个字符串是冒号,最后一个字符串也是冒号。如果是服务端渲染,会使用大写字母 R 作为标识,并获取组件树的 treeId 拼接到 id 中,如果是客户端渲染,会使用小写字母 r 作为标识。

在服务端渲染中,使用了 localIdCounter 变量来记录 useId 在页面中被使用的次数,在客户端中使用 globalClientIdCounter 变量来记录 useId 在页面中被使用的次数,因此,即使 useId 在页面中出现多次,生成的 ID 也不会冲突。

最后将生成的id保存到 hook 对象的 memoizedState 属性上,并返回缓存后的值:

// 将生成的唯一 id 存储到 hook 对象的 memoizedState 属性上
hook.memoizedState = id;
return id;

2.2 mountWorkInProgressHook

在 mountId() 函数中,使用 mountWorkInProgressHook() 函数创建了一个新的 hook 对象,我们来看看它是如何被创建的:

// packages/react-reconciler/src/ReactFiberHooks.js

// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };
  
   // Hooks are stored as a linked list on the fiber's memoizedState field
  // 将 新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上

  // 只有在第一次打开页面的时候,workInProgressHook 为空
  if (workInProgressHook === null) {
    // This is the first hook in the list
    // 链表上的第一个 hook
    
    // currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,也同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性,如果 workInProgressHook 不为 null,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。

3、更新阶段

组件在更新阶段,执行 useId,实际上执行的是 updateId,下面我们来看看 updateId 的实现。

3.1 updateId

function updateId(): string {
  // 获取当前的 workInProgressHook
  const hook = updateWorkInProgressHook();
  // 从当前的 workInProgressHook 上获取 useId 生成的 id,因此即使组件重新渲染,id 也不会变化
  const id: string = hook.memoizedState;
  return id;
}

在更新阶段,执行 useId,首先会调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber 节点上的 workInProgressHook 对象:

接着从 workInProgressHook 对象上获取 useId 生成的 id,因此即使组件重新渲染,useId 生成的 id 也不会变。

4、useId 流程图

React Hooks 源码解读之 useId

5、总结

useId 用于生成一个唯一的字符串 ID,在使用这个 hook 时需要注意的是,不能在循环语句或条件语句中使用。

useId 生成的字符串ID,第一个字符是冒号,最后一个字符也是冒号,如果是服务端渲染生成的ID,则会使用大写字母 R 作为标识,如果是客户端渲染生成的ID,则会使用小写字母 r 作为标识。

useId 生成字符串ID时,会使用一个全局变量来记录 useId 在页面中出现的次数,因此,即使 useId 在页面中出现多次,生成的 ID 也不会冲突。