React 中有一个 useId hook,可以生成一个唯一 ID,这个有什么用处呢,用个 UUID 是不是可以替代呢?如果我们只考虑客户端,那么生成唯一 Id 的方法比较简单,我们在 State 中保存一个计数器就好,但是 React 需要支持服务端渲染,需要确保服务器生成的内容与客户端保持一致。客户端和服务器端是完全不同的环境,通过简单的计数器无法确保两端的一致。我们看下面这个例子,例子中要将 label 和 输入框进行关联,可以 useId 这个 hook 来实现。通过简单的计数器无法实现
import { useId } from 'react';export default function Form() {const id = useId();return (<form><label htmlFor={id + '-firstName'}>First Name:</label><input id={id + '-firstName'} type="text" /><hr /><label htmlFor={id + '-lastName'}>Last Name:</label><input id={id + '-lastName'} type="text" /></form>);
}
React 是通过 dom tree 的位置来实现的,不同层有不同的前缀,如下:
<div><div id="1"><div id="101" /></div><div id="10" />
</div>
下面代码 React 源码中的 Id 生成算法,通过位置进行移位,并对索引进行相加。
export function pushTreeId(workInProgress: Fiber,totalChildren: number,index: number,
) {warnIfNotHydrating();idStack[idStackIndex++] = treeContextId;idStack[idStackIndex++] = treeContextOverflow;idStack[idStackIndex++] = treeContextProvider;treeContextProvider = workInProgress;const baseIdWithLeadingBit = treeContextId;const baseOverflow = treeContextOverflow;// The leftmost 1 marks the end of the sequence, non-inclusive. It's not part// of the id; we use it to account for leading 0s.const baseLength = getBitLength(baseIdWithLeadingBit) - 1;const baseId = baseIdWithLeadingBit & ~(1 << baseLength);const slot = index + 1;const length = getBitLength(totalChildren) + baseLength;// 30 is the max length we can store without overflowing, taking into// consideration the leading 1 we use to mark the end of the sequence.if (length > 30) {// We overflowed the bitwise-safe range. Fall back to slower algorithm.// This branch assumes the length of the base id is greater than 5; it won't// work for smaller ids, because you need 5 bits per character.//// We encode the id in multiple steps: first the base id, then the// remaining digits.//// Each 5 bit sequence corresponds to a single base 32 character. So for// example, if the current id is 23 bits long, we can convert 20 of those// bits into a string of 4 characters, with 3 bits left over.//// First calculate how many bits in the base id represent a complete// sequence of characters.const numberOfOverflowBits = baseLength - (baseLength % 5);// Then create a bitmask that selects only those bits.const newOverflowBits = (1 << numberOfOverflowBits) - 1;// Select the bits, and convert them to a base 32 string.const newOverflow = (baseId & newOverflowBits).toString(32);// Now we can remove those bits from the base id.const restOfBaseId = baseId >> numberOfOverflowBits;const restOfBaseLength = baseLength - numberOfOverflowBits;// Finally, encode the rest of the bits using the normal algorithm. Because// we made more room, this time it won't overflow.const restOfLength = getBitLength(totalChildren) + restOfBaseLength;const restOfNewBits = slot << restOfBaseLength;const id = restOfNewBits | restOfBaseId;const overflow = newOverflow + baseOverflow;treeContextId = (1 << restOfLength) | id;treeContextOverflow = overflow;} else {// Normal pathconst newBits = slot << baseLength;const id = newBits | baseId;const overflow = baseOverflow;treeContextId = (1 << length) | id;treeContextOverflow = overflow;}
}
这个Id 算法只有服务器渲染才需要,如果只是客户端渲染无需这么复杂的计算,React 也做了这样的处理,代码如下,客户端渲染直接使用全局计数器。
总结
useId 这个 Hook,是为了服务端渲染的需求才有的,客户端渲染无需这种复杂计算。不同层使用不同的前缀,通过移位进行区分。