前言

2024 年 9 月 1 日,Vue 3.5“天元突破:红莲螺岩”版本发布完整更新日志

这次更新包含内部改进和实用的新功能,Vue 变得更加好用了!

在 3.5 中,Vue 的响应式系统经历了一次重大重构,在行为没有变化的情况下,实现了更好的性能和显著改善的内存使用率(-56%)。重构还解决了 SSR 期间计算值挂起导致的过时计算值和内存问题。
此外,3.5 还优化了大型、深度反应阵列的反应性跟踪,在某些情况下可使此类操作速度提高 10 倍。

响应式 Props 解构

终于能够使用原生 JS 的语法来响应式地解构 props 了!文档

先写一个父组件。

src\App.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<Hello :count="num"></Hello>
<button @click="add">增加</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Hello from "./components/Hello.vue";
const num = ref(0);
const add = () => {
num.value++;
};
</script>

在以前,需要使用 withDefaults() 提供默认值,再使用 toRefs() 解构 props。

src\components\Hello.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h2>{{ count }}</h2>
</div>
</template>
<script setup lang="ts">
import { toRefs, watchEffect } from "vue";
const props = withDefaults(
defineProps<{
count?: number;
}>(),
{
count: 0,
}
);
const { count } = toRefs(props);
watchEffect(() => {
console.log(count.value);
});
</script>

而现在,能使用原生 JS 的语法来解构 props,并指定默认值。

src\components\Hello.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { toRefs, watchEffect } from "vue";

const { count = 0 } = defineProps<{
count?: number;
}>();

watchEffect(() => {
// console.log(count.value);
console.log(count);
});

// const props = withDefaults(
// defineProps<{
// count?: number;
// }>(),
// {
// count: 0,
// }
// );
// const { count } = toRefs(props);

VSC 插件优化

即使是基本类型,也不需要 .value。这个行为或许会带来一定的困惑。

所以 Vue - Official 插件也提供了一个视觉优化 vue.inlayHints.destructuredProps 选项,对于从 props 解构的变量,会在其左侧显示 props. 提示。

原理

在 vite 中外化 vue 依赖并取消压缩代码,方便查看构建产物

1
2
3
4
5
6
7
8
9
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
external: ["vue"],
},
minify: false,
},
});

在产物中可以看到,在编译时,count 会被转换为 __props.count,当然就不会丢失响应式了。

1
2
3
watchEffect(() => {
console.log(__props.count);
});

所以使用 watch 监视解构的 prop 变量时,需要使用 getter 函数包裹。

1
2
watch(count /* ... */) // 编译时会报错
watch(() => count /* ... */) // 正确

useTemplateRef 获取模板引用实例

可以使用 useTemplateRef() 获取模板引用实例,而不需要预先准备一个 ref。这有助于区分模板引用和响应式数据,且更符合原生获取 DOM 元素的语法习惯。文档

1
2
3
4
5
6
7
8
9
10
// 子组件
const data = ref("Hello World");
defineExpose({ data });

// 父组件
import { useTemplateRef } from "vue";
const helloRef = useTemplateRef('helloRef');
onMounted(() => {
console.log(helloRef.value?.data); // Hello World
});

内置 Teleport 组件优化

内置 <Teleport> 组件的一个已知限制是,其目标元素必须在 <Teleport> 组件挂载时已存在。

在 3.5 中,<Teleport> 组件新增了 defer 属性,可以在当前渲染周期之后再挂载。

1
2
3
4
5
6
7
8
9
<Teleport defer to="#cont">
<div v-if="open">
<span>挂载到id为cont的div上</span>
<button @click="open = false">关闭</button>
</div>
</Teleport>
<!-- Teleport组件传送内容的容器,在 Teleport 之后渲染-->
<div id="cont"></div>
<button @click="open = true">打开</button>

因为 #cont 在 <Teleport> 之后渲染,所以之前的 <Teleport> 无法将内容传送到 #cont。

1
2
3
main.ts:4 [Vue warn]: Failed to locate Teleport target with selector "#cont". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree. 
at <Dialog>
at <App>

watch 函数相关

之前 watch 函数是和 Vue 组件以及生命周期一起实现的,源码在 runtime-core 模块中,有时只需要使用 vue 的响应式功能,即 reactivity 模块,但不得不引入 runtime-core 模块。

3.5 在 reactivity 模块中实现了一个和原来 watch API 一模一样的 watch,用法没有区别。详见:Vue3.5新增的baseWatch让watch函数和Vue组件彻底分手-前端欧阳

onWatcherCleanup

onWatcherCleanup() 用于在 watch 中注册清理回调。在组件卸载之前或者下一次 watch 回调执行之前会自动调用清理回调。

1
2
3
4
5
6
7
8
9
watch(num, () => {
const timer = setInterval(() => {
console.log("do something");
}, 200);
onWatcherCleanup(() => {
console.log("清理定时器");
clearInterval(timer);
});
});

不必再手动在 onBeforeUnmount 钩子中清理 watch 产生的副作用。

pause 和 resume

pauseresume 用于暂停和恢复 watch 的执行。

暂停后,watch 回调不会执行,恢复时,会立即执行一次回调

1
2
3
4
5
6
<button @click="watcher.pause()">暂停 watch</button>
<button @click="watcher.resume()">恢复 watch</button>

const watcher = watch(num, () => {
console.log(`num changed: ${num.value}`);
});

deep 支持传入数字

以前 deep 只能传入布尔值,现在支持传入数字,表明监控对象的深度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj1 = ref({
x: 0,
a: {
b: 1,
},
});
setTimeout(() => {
obj1.value.x = 1;
setTimeout(() => {
obj1.value.a.b = 2;
}, 1000);
}, 1000);

watch(
obj1,
() => {
console.log("obj1 changed"); // 只执行一次,因为 obj1.value.a.b 深度为 2。
},
{
deep: 1,
}
);

SSR服务端渲染优化

3.5 中,SSR 期间计算值挂起导致的过时计算值和内存问题得到解决。

新增 useId 函数、Lazy Hydration 懒加载水合、data-allow-mismatch 等。

没用过 vue 的 SSR 就不看这块了,详见:文档

参考

Announcing Vue 3.5
这应该是全网最详细的Vue3.5版本解读
vue3.5最新发布,这几个新用法还不快来学习一下!