大家好,我是欧阳,又跟大家见面啦!
欧阳建了一个高质量vue源码交流群,群里还有不少面试官,扫描文末的二维码加入微信群。
React在很早之前的版本中加了useId
,用于生成唯一ID。在Vue3.5版本中,终于也有了期待已久的useId
。这篇文章来带你搞清楚useId
有哪些应用场景,以及他是如何实现的。
他的作用也是生成唯一ID,同一个Vue应用里面每次调用useId
生成的ID都不同。
使用方法也很简单,代码如下:
<script setup lang="ts">
import { useId } from 'vue'
const id0 = useId();
console.log(id0); // v-0
const id1 = useId();
console.log(id1); // v-1
const id2 = useId();
console.log(id2); // v-2
</script>
看到这里有的小伙伴会有问题,你上面的例子都是在同一组件里面调用useId
。那如果我在不同的组件里面分别调用useId
,这些组件生成的ID还是唯一的吗?
比如下面这个例子,父组件代码如下:
<template>
<div>
<UseIdChild1 />
<UseIdChild2 />
</div>
</template>
子组件UseIdChild1
代码如下:
<script setup lang="ts">
import { useId } from "vue";
const id0 = useId();
const id1 = useId();
console.log(id0);
console.log(id1);
</script>
子组件UseIdChild2
代码如下:
<script setup lang="ts">
import { useId } from "vue";
const id0 = useId();
const id1 = useId();
console.log(id0);
console.log(id1);
</script>
从上面的代码可以看到两个子组件里面的代码实际是一样的,那你猜猜子组件UseIdChild1
中打印的id0
、id1
和子组件UseIdChild2
中打印的id0
、id1
是不是一样的呢?
答案是:不一样。
UseIdChild1
中打印的id0
的值为v-0
,id1
的值为v-1
。
UseIdChild2
中打印的id0
的值为v-2
,id1
的值为v-3
。
通过上面的这两个例子,我想你应该猜出来useId
函数生成唯一ID的规律:“字符串v-
加上自增的数字
”。
其中的前缀v
可以通过app.config.idPrefix
进行自定义。
有的时候我们要渲染一个列表数据,需要列表的每一个item中有一个唯一的id,此时我们就可以使用useId
去给每个item生成唯一的id。
这个是最简单的使用场景,接下来我们看看在服务端渲染(SSR)中useId
的使用场景。
首先我们要搞清楚服务端渲染时有哪些痛点?
我们来看一个服务端渲染的例子,代码如下:
<template>
<div>
<label :htmlFor="id">Do you like Vue3.5?</label>
<input type="checkbox" name="vue3.5" :id="id" />
</div>
</template>
<script setup lang="ts">
const id = Math.random();
</script>
上面的代码如果是跑在客户端渲染时没有任何问题,但是如果在服务端渲染时就会有警告了。如下图:
上面的警告意思是,在服务端时生成的id的值为0.4050816845323888
。但是在客户端时生成的id的值却是0.4746900241123273
,这两次生成的id值不同,所以才会出现警告。
可能有的小伙伴会有疑问,为什么在服务端生成一次id后,在客户端又去生成一次id呢?
为了解答上面这个问题,我们先来了解一下服务端渲染(SSR)的流程:
dehydrate
(脱水)。hydrate
(注水)。由于我们这里是使用Math.random()
去生成的id,在服务端和客户端每次执行Math.random()
生成的id值当然就不同了,所以才会出现上面的警告。
有了useId
后,解决上面的警告就很简单了,只需要把Math.random()
改成useId()
就可以了。代码如下:
<template>
<div>
<label :htmlFor="id">Do you like Vue3.5?</label>
<input type="checkbox" name="vue3.5" :id="id" />
</div>
</template>
<script setup lang="ts">
const id = useId();
</script>
因为useId
在服务端渲染时会生成v-0
,在客户端渲染时依然还是v-0
。
可能有的小伙伴有疑问,前面不是讲的useId
每执行一次会给后面的数字+1
。那么服务端执行一次后,再去客户端执行一次,讲道理应该生成的ID不一样吧??
useId
生成的“自增数字部分”是维护在vue实例上面的ids
属性上,服务端渲染时会在Node.js端生成一个vue实例。但是客户端渲染时又会在浏览器中重新生成一个新的vue实例,此时vue实例上的ids
属性也会被重置,所以在服务端和客户端执行useId
生成的值是一样的。
我们来看看useId
的源码,非常简单!!简化后的代码如下:
function useId(): string {
const i = getCurrentInstance()
if (i) {
return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++
}
return ''
}
这个getCurrentInstance
函数我想很多同学都比较熟悉,他的作用是返回当前vue实例。
给useId
打个断点,来看一下当前vue实例i
,如下图:
从上图中可以看到vue实例上的ids
属性是一个数组,数组的第一项是空字符串,第二项是数字0,第三项也是数字0
我们再来看看useId
是如何返回唯一ID的,如下:
return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++
生成的唯一ID由三部分组成:
app.config.idPrefix
中取的。如果没有配置,那么就是字符串v
。-
。i.ids[0] + i.ids[1]++
,其中ids[0]
的值为空字符串。i.ids[1]++
这里是先取值,然后再执行++
,所以第三部分的值为数字0
。再次调用useId
时,由于上一次执行过一次++
了。此时的数字值为1
,并且再次执行++
。看到这里有的小伙伴又有疑问了,这里看上去ids
属性是存在vue实例上面的。每个vue组件都有一个vue实例,那么每个组件都有各自维护的ids
属性。那你前面的那个例子中UseIdChild1
子组件和UseIdChild2
子组件中各自生成的id0
的值应该是一样的v-0
吧,为什么一个是v-0
,另外一个是v-2
呢?
答案其实很简单,所有vue实例上面的ids
属性都是同一个数组,指向的是顶层组件实例上面的那个ids
属性。创建vue实例的源码如下图:
从上图中可以看到当没有父组件时,也就是最顶层的vue组件实例,就将其ids
属性设置为数组['', 0, 0]
。
当生成子组件的vue实例时,由于父组件上面有ids
属性,所以就用父组件上面的了。指针都是指向的是最顶层vue实例上面的ids
属性,所以才会说所有的vue组件实例上面的ids
属性都是指向同一个数组。
这也就是为什么UseIdChild1
子组件和UseIdChild2
子组件中各自生成的id0
的值一个是v-0
,另外一个是v-2
。
Vue3.5新增的useId
可以在Vue应用内生成唯一的ID,我们可以使用useId
给列表数据中的每一个item生成一个唯一的id。
并且在服务端渲染(SSR)场景中,服务端和客户端执行useId
生成的是同一个ID。利用这个特点我们可以使用useId
解决一些在 SSR 应用中,服务器端和客户端生成的 ID 不一致导致的警告。
最后我们讲了useId
的实现也很简单,生成的ID分为三部分:
app.config.idPrefix
,如果没有配置,那么就是字符串v
。-
。ids
属性,所有的vue实例上面的ids
属性都是指向同一个数组。这也就是为什么说useId
可以在Vue应用内
生成唯一的ID,而不是在Vue组件内
生成唯一的ID。