Vue 3.5 的 props 响应式解构和 useTemplateRef 改变了什么?
Vue 3.5 起,从 defineProps 解构出的变量会被编译器改写成 props.xxx,所以仍然响应式,还能用 JS 默认值语法代替 withDefaults;但传给 watch 或组合式函数时要包成 getter。useTemplateRef('name') 按运行时字符串匹配模板里的 ref 属性,不再要求变量名和 ref 名一致,动态 ref 也能用。
简答
Vue 3.5 是 2024 年 9 月发布的小版本,没有破坏性变更。和日常写代码关系最大的两处,一个是 props 解构,一个是模板引用。
以前
const { count } = defineProps(...)解构出来的count是个常量,父组件改了它也不变。3.5 起这个特性稳定并默认开启,编译器会把同一个<script setup>里用到的count改写成props.count,所以它是响应式的;默认值也能直接写成{ count = 0 },不再需要withDefaults。模板引用以前要声明一个和
ref="input"同名的const input = ref(null),靠变量名对上。3.5 新增useTemplateRef('input'),按运行时的字符串 ID 匹配,变量名随便起,ref 名是动态的也能用,配合工具还能推断类型。要注意,解构出的 prop 直接传给
watch(count)编译时会报错,要写成watch(() => count);传给组合式函数也要包成 getter。模板引用要等组件挂载后才有值,挂载前是null。
props 解构到底发生了什么
先看编译前后的对比。下面这段 <script setup>:
<script setup>
import { watch } from 'vue';
const { count = 0 } = defineProps(['count']);
watch(() => count, () => {});
console.log(count);
</script>
用 @vue/compiler-sfc 3.5 编译后,setup 里的关键部分变成了这样:
watch(() => __props.count, () => {});
console.log(__props.count);
所以 count 并不是真的被解构成了一个普通变量。每次访问都被换成了对 props 对象的属性读取,Vue 的响应式靠属性访问收集依赖,这样就能追踪到。默认值 count = 0 则被编译成了组件的 props 默认值声明。
那为什么 watch(count, ...) 不行?按改写规则,它会变成 watch(__props.count, ...),传进去的是一个值,不是响应式数据源。Vue 编译器会直接报错,提示改成 () => count。同样,把解构的 prop 传给组合式函数时要写 useComposable(() => count),函数内部用 toValue() 读取。这个规则和版本差别在《defineProps、defineEmits 和 defineExpose 有什么作用?》《为什么 Vue 提倡 props 单向数据流?》里也有完整例子。
useTemplateRef 解决了什么麻烦
3.5 以前,<script setup> 里拿 DOM 元素要这样写:
<script setup>
import { ref, onMounted } from 'vue';
const input = ref(null);
onMounted(() => {
input.value.focus();
});
</script>
<template>
<input ref="input" />
</template>
这里的约定是变量名必须和 ref 属性的值完全一样。读代码时看不出 input 是个模板引用,改名时两边也得一起改。
3.5 以后写成:
<script setup>
import { useTemplateRef, onMounted } from 'vue';
const input = useTemplateRef('my-input');
onMounted(() => {
input.value.focus();
});
</script>
<template>
<input ref="my-input" />
</template>
区别在于匹配方式。以前是编译器分析静态的变量名,现在是运行时按字符串 ID 匹配,所以 ref 名可以带连字符,也可以是动态绑定、会变化的 ID。用 TypeScript 时,IDE 和 vue-tsc 会根据 ref 用在哪个元素或组件上自动推断 input.value 的类型。
v-for 里用 ref="items" 时,useTemplateRef('items') 拿到的是一个数组,挂载后填入元素。官方文档提醒,这个数组的顺序不保证和源数组一致。ref 加在组件上拿到的是组件实例,<script setup> 组件默认什么都不暴露,要用 defineExpose。
3.5 还带来了哪些变化
官方发布文章还列了这些:响应式系统重构,内存占用减少 56%,某些情况下大型深层响应式数组的追踪快了最多 10 倍;onWatcherCleanup() 用于在侦听器里注册清理,见《computed、watch 和 watchEffect 应该如何选择?》;<Teleport defer> 让目标元素可以晚于 Teleport 渲染,见《KeepAlive 与 Teleport 有哪些使用边界?》。
服务端渲染方面有三项:defineAsyncComponent() 支持 hydrate 选项做懒水合,比如 hydrateOnVisible();useId() 生成在服务端和客户端保持一致的 ID;data-allow-mismatch 用来消除服务端和客户端值必然不同时的水合警告,比如日期。
常见追问
升级到 3.5 后,原来用 withDefaults 的代码要改吗?
不用。3.5 没有破坏性变更,
withDefaults照样能用。只是新代码可以直接写解构默认值,更简洁。
解构的 prop 和普通变量在编辑器里怎么区分?
官方提到 Vue 的 VS Code 扩展有一个设置,可以给解构出来的 props 显示内联提示,开启后就能一眼看出来。
useTemplateRef 在 setup 里立刻读,为什么是 null?
因为元素要第一次渲染后才存在。官方建议在
onMounted里访问;用watchEffect时要判断input.value是否存在,元素被v-if移除后它也会变回null。