Skip to main content

Vue3 新特性

一、版本时间线

版本发布时间重点特性
3.02020.09Composition API、Teleport、Fragment、<script setup> 实验性
3.22021.08<script setup> 稳定、v-memo、CSS v-bind、<style> 响应式变量
3.32023.05defineModel 实验、defineOptionsdefineSlots
3.42023.12重写模板解析器(速度 +2 倍)、包体积 -50%、defineModel 稳定
3.52024.09响应式 props 解构、useTemplateRefuseId、懒加载 ref

重点:3.4 是性能拐点(虚拟 DOM 重写 + 解析器重写),3.5 是 DX 拐点(三个 useXxx 钩子 + 解构不丢响应式)。面试时把这两个版本讲清楚基本够用。


二、编译时宏(<script setup> 系列)

这是 Vue 3 写法的核心。宏在编译阶段会被翻译成普通函数,所以不需要手动 import。

2.1 <script setup> —— 推荐的组件写法

<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
<button @click="count++">{{ count }}</button>
</template>

核心收益

维度不用 <script setup><script setup>
顶层变量暴露必须 return自动暴露给模板
defineComponent 包裹需要不需要
父子通信emits 选项defineEmits
TS 类型推导一般✅ 完美

2.2 defineProps —— 父传子

<script setup lang="ts">
// 运行时声明
const props = defineProps({
msg: { type: String, required: true },
count: { type: Number, default: 0 },
});

// 类型声明(TS 写法,**推荐**)
const props = defineProps<{
msg: string;
count?: number;
tags?: string[];
}>();
</script>

2.3 defineEmits —— 子传父

<script setup lang="ts">
// 类型声明写法(推荐)
const emit = defineEmits<{
change: [value: string]; // 元组形式传参数
update: [id: number];
}>();

emit("change", "hello");
emit("update", 123);
</script>

3.4 改版:之前的 change: (value: string) => void 在 3.4 后改成了元组形式 change: [value: string],更直观。旧写法仍兼容但会报 ESLint 警告。

2.4 defineExpose —— 暴露给父组件

<script setup> 默认不暴露任何东西给父组件。父组件用 ref 也拿不到子组件的方法/数据,必须用 defineExpose 显式暴露:

<!-- Child.vue -->
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => count.value++;

defineExpose({
count, // 暴露 ref
increment, // 暴露方法
});
</script>
<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from "vue";
import Child from "./Child.vue";

const childRef = ref(null);

onMounted(() => {
// ✅ 能拿到,因为 Child 显式 expose 了
childRef.value?.increment();
console.log(childRef.value?.count);
});
</script>

<template>
<Child ref="childRef" />
</template>

2.5 defineModel(3.4 稳定)—— v-model 的宏写法

3.4 之前要手写 defineProps + defineEmits

<!-- 3.4 之前:繁琐 -->
<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

3.4 之后defineModel 一行搞定:

<!-- 3.4+:推荐 -->
<script setup>
const model = defineModel(); // 默认绑定 modelValue
const title = defineModel("title"); // 自定义 v-model:title
const count = defineModel("count", { default: 0, type: Number });
</script>

<template>
<input v-model="model" />
<input v-model="title" />
</template>

设计意图:把"声明 props + 转发"两步合成一步,消灭模板里的样板代码。v-model 的本质没变——还是 props + emit,只是写法简化了。

2.6 defineOptions(3.3+)—— 在 <script setup> 里声明组件选项

<script setup>
defineOptions({
name: "MyComponent", // ⚠️ 之前在 setup 里没办法声明 name
inheritAttrs: false,
customOptions: {
/* ... */
},
});
</script>

痛点场景:在 <script setup> 模式下,组件 name 只能通过 defineComponent 或额外 script 块来设。3.3 之后直接用 defineOptions 就能声明。

2.7 defineSlots(3.3+)—— 类型化 slot

<script setup lang="ts">
const slots = defineSlots<{
default(props: { item: Item }): any;
header(): any;
}>();
</script>

三、组合式 API 增强

3.1 useTemplateRef(3.5+)—— 类型友好的 ref

3.5 之前

<script setup>
import { ref } from "vue";
const inputRef = ref(null); // 类型推导成 Ref<null>,需要手动断言
</script>
<template><input ref="inputRef" /></template>

3.5 之后

<script setup>
import { useTemplateRef } from "vue";
const inputRef = useTemplateRef("inputRef"); // 名字必须和 template 里 ref 一致
</script>
<template><input ref="inputRef" /></template>

设计意图:把"声明 ref 变量"和"模板里 ref 字符串"强制绑定,减少拼写错误。字符串"inputRef"必须唯一且和 ref 一致

3.2 useId(3.5+)—— SSR 安全的唯一 ID

<script setup>
import { useId } from "vue";
const uid = useId(); // 每次调用都返回新的唯一 ID
</script>

<template>
<label :for="uid">用户名</label>
<input :id="uid" type="text" />
</template>

crypto.randomUUID() 的区别useId 在 SSR 模式下保证客户端和服务端 hydration 时 ID 一致(避免 hydration mismatch),randomUUID 不能保证。

3.3 响应式 props 解构(3.5+)—— 解构不丢响应式

3.5 之前(解构 props 会丢失响应式):

<script setup>
const props = defineProps(["count"]);
const { count } = props; // ❌ count 是普通常量了,不会再响应式更新
</script>

3.5 之后(用 default 函数 + 直接解构):

<script setup>
const { count = 0, double = () => count.value * 2 } = defineProps([
"count",
"double",
]);
// ✅ count 是响应式的,可以直接在模板和 watch/watchEffect 里用
</script>

设计点:编译器把解构形式"翻译"成原来的 props 访问,对开发透明。这是 3.5 DX 提升最明显的一处——以前要么不解构、要么手写 computed 包一层。

3.4 effectScope —— 显式管理副作用作用域

<script setup>
import { effectScope, ref, watch } from "vue";

const scope = effectScope();

scope.run(() => {
const count = ref(0);
watch(count, (v) => console.log(v));
});

// 一次性清理:手动停止所有 effect
scope.stop();
</script>

典型场景:在组合式函数(自定义 hook)里用,自动绑定到调用方的作用域。例如 pinia 内部就是用 effectScope 让 store 的所有 effect 在组件卸载时自动清理。


四、模板与样式新特性

4.1 v-memo(3.2+)—— 跳过整棵子树的重渲染

<template>
<!-- 只有当 v-memo 依赖变化时才重新渲染这块 -->
<div v-memo="[item.id, item.updatedAt]">
<HeavyChart :data="item" />
<RichTextEditor :content="item.content" />
</div>
</template>

典型场景:万行列表里每行有富文本/图表/视频等重组件,只对可见的 N 行启用 v-memo,性能立竿见影。

对比 v-oncev-once 只渲染一次;v-memo 是"依赖变了才重渲"——更灵活。

4.2 CSS 变量 v-bind(3.2+)—— 响应式样式

<script setup>
import { ref } from "vue";
const color = ref("#42b883");
</script>

<template>
<div class="box">响应式颜色</div>
</template>

<style scoped>
.box {
/* 直接在 CSS 里绑定 JS 状态 */
color: v-bind(color);
/* 复杂值需要字符串包裹 */
background: v-bind("`rgb(${r}, ${g}, ${b})`");
}
</style>

编译原理:Vue 把 v-bind(color) 编译成 --xxx-xxx-color CSS 变量,再在 DOM 节点的 style 上动态写入。真正做到了"CSS 跟着 JS 状态走"

优势:不用给每个动态值加 :style,CSS 代码可读性更高。


五、响应式系统 API 增强

5.1 toRef / toRefs / toValue(3.3+)

import { toRef, toRefs, toValue, ref, reactive } from "vue";

// toRef:从 reactive 对象里取一个属性的 ref
const state = reactive({ count: 0 });
const countRef = toRef(state, "count"); // 双向同步

// toRefs:批量转换(解构 reactive 必备)
const { count, name } = toRefs(state);

// toValue(3.3+):统一读取 ref 或 getter(之前叫 unref 的增强版)
const value = toValue(maybeRef); // 内部判断 ref/reactive/raw

典型场景

  • toRef:把 props 转换成 ref(3.5 后 props 解构已经原生支持响应式,这个用法变少了)
  • toRefs:在 <script setup>解构 reactive 对象而不丢响应式
  • toValue:写通用工具时统一处理 ref/reactive/raw 值

5.2 shallowRef / shallowReactive —— 浅响应

import { shallowRef, triggerRef } from "vue";

// 大对象只需要最外层响应式 → 用 shallowRef
const bigData = shallowRef({ items: [...] });

// 修改对象本身 → 自动触发
bigData.value = { items: newData };

// 修改内部属性 → ❌ 不会触发
bigData.value.items.push(...); // 必须 triggerRef 手动通知
triggerRef(bigData);

性能场景:地图数据、ECharts 配置、Canvas 帧数据——只关心整体替换,不需要深度响应。省下大量 Proxy 拦截开销

5.3 markRaw —— 标记永不响应

import { reactive, markRaw } from "vue";

const map = reactive({
instance: markRaw(new AMap.Map(...)), // 标记为原始对象,跳过代理
});

典型场景:集成第三方库(地图、图表、富文本)的实例对象,强转为非响应式避免性能损耗。


六、内置组件增强

6.1 <Teleport> —— 把内容传送到任意 DOM 位置

<template>
<button @click="show = true">打开弹窗</button>
<Teleport to="body">
<div v-if="show" class="modal">弹窗内容</div>
</Teleport>
</template>

典型场景:弹窗、Toast、Notification、Dialog 等需要脱离父组件 z-index 层级的元素。

优势:视觉上脱离了父组件,但逻辑上(事件冒泡、provide/inject)依然属于原组件

6.2 <Suspense> —— 异步组件的统一处理

<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncUserProfile />
</template>
<!-- 加载中 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
// AsyncUserProfile.vue
<script setup>
// async setup 会被 Suspense 识别
const data = await fetch("/api/user");
</script>

状态:仍是实验性。Nuxt 3 大量使用。原生项目慎用,生产环境可能出现边界 bug

6.3 Fragment(多根节点)—— 组件不再要求单根

<template>
<header>标题</header>
<main>内容</main>
<footer>底部</footer>
</template>

2.x 时代必须用单根包一层 + v-if,3.x 直接支持多根。但要注意 attribute 透传会有 warning——需要显式用 defineOptions({ inheritAttrs: false })


七、性能优化里程碑(3.4+)

Vue 3.4 是一次底层全面重写,性能提升巨大:

维度3.3 → 3.4 提升原理
模板解析速度+2 倍重写解析器,更激进的单态优化
包体积-50%(部分场景)删除大量兼容代码 + 路径内联
内存占用更低优化 VNode 创建逻辑
响应式系统全面重写单态 effect 复用,去掉 _dirty flag

3.5 进一步优化

  • 懒加载 ref:把 ref 改为惰性,只有访问 .value 时才计算
  • 解构不丢响应式(见 3.3 节)
  • 内存占用进一步降低

八、状态管理:Pinia

Vue 3 官方推荐的状态管理库,已取代 Vuex

// stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useUserStore = defineStore("user", () => {
const name = ref("lucas");
const age = ref(18);
const isAdult = computed(() => age.value >= 18);

const birthday = () => {
age.value++;
};

return { name, age, isAdult, birthday };
});
<!-- 组件里使用 -->
<script setup>
import { useUserStore } from "@/stores/user";
import { storeToRefs } from "pinia";

const userStore = useUserStore();
// 解构 store 不丢响应式
const { name, age, isAdult } = storeToRefs(userStore);
</script>

为什么 Pinia 取代 Vuex

维度VuexPinia
写法state / mutations / actions / modules只有 state / getters / actions没有 mutations
TS 支持弱(依赖 vuex-class 等库)✅ 完美(函数式 + 类型推导)
模块化需要嵌套 + namespace默认扁平,defineStore 自带命名空间
同步异步mutations 只能同步、actions 才能异步actions 同步异步都 OK
体积较大小,API 简洁

九、面试高频 Q&A

Q1: <script setup> 相比普通 setup 有什么好处?

  • 自动暴露顶层变量给模板,不需要 return
  • 不需要 defineComponent 包裹
  • 编译时自动优化(static hoisting、cache 变量等)
  • TS 类型推导更友好

Q2: defineModel 和手写 props + emit 有什么本质区别?

没有本质区别——defineModel 是 props + emit 的语法糖。源码层面:

const model = defineModel();
// 等价于:
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
// model.value = emit("update:modelValue", ...)

优势是消灭样板代码,不是新增能力。

Q3: 为什么 Vue 3.5 解构 props 不会丢响应式?

编译器把解构形式"翻译"成普通的 props 访问:

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

// 编译后
const props = defineProps<{ count?: number }>();
const count = computed(() => props.count ?? 0);

解构出来的 count 实际上是 computed ref,保持了响应式。这是 3.5 编译器增强的能力,对开发完全透明。

Q4: v-memov-once 区别?

  • v-once:只渲染一次,永不更新
  • v-memo="[deps]":当 deps 数组每个值都和上次一样时跳过更新,否则正常重渲染

性能场景:v-memo 更灵活。长列表里只对不可变数据的行用 v-memo,对动态数据用正常 v-for。

Q5: shallowRef 什么时候用?

  • 大数据对象(地图、图表、Canvas 帧数据)
  • 整体替换、不需要深度响应
  • 内部属性变化时手动调 triggerRef 通知更新

Q6: Teleport 的事件冒泡还在原组件吗?

。Teleport 改变的是 DOM 位置,但 Vue 组件树不变——事件冒泡、provide/inject 依然在原组件层级生效。

这点和 React 的 Portal 设计意图一致,避免了"DOM 位置 vs 逻辑位置"不一致带来的混乱。

Q7: Pinia 为什么要用 storeToRefs

直接解构 store 会丢失响应式(因为解构出来的是普通值):

const { name } = userStore; // ❌ name 是字符串常量

storeToRefs 把 state 和 getters 转换成 ref:

const { name, isAdult } = storeToRefs(userStore); // ✅ 都是 ref

而 actions 不需要转换,可以直接解构(因为它们是函数)。

Q8: Vue 3 相比 Vue 2 最本质的三个升级?

  1. 响应式原理Object.definePropertyProxy(能监听数组下标和新增属性)
  2. 组织方式:Options API → Composition API(逻辑聚合,TS 友好)
  3. 编译优化:全量 VNode Diff → 靶向更新(PatchFlags + Block Tree + Static Hoisting)

Q9: Vue 3 抛弃的 filter 怎么替换?

  • 模板用 {{ x | capitalize }}{{ capitalize(x) }}(直接用函数)
  • 全局注册用 app.config.globalProperties.$filter = { ... }

原因:filter 难以调试、this 上下文难处理,函数调用更直观

Q10: 3.4 之后还能用 Vue 2 写新项目吗?

技术上可以(@vue/compat 兼容包),但不推荐。Vue 2 已于 2023.12 停止维护,EOL 状态。新项目直接 Vue 3,老项目按团队节奏逐步迁移。