Skip to contentsetup 与
Vue3 新特性
概述
Vue 3 保留了 Vue 2 的核心设计,同时对组件组织方式、响应式系统以及若干内置能力进行了较大调整。主要变化如下:
- Composition API:提供了一套基于函数组合的逻辑复用机制,替代 Options API 分散的组件选项。
<script setup>语法糖:在单文件组件中以更精简的写法使用 Composition API。- Teleport 组件:支持将模板的一部分渲染到当前组件 DOM 树之外的指定位置。
- Suspense 组件:为异步组件提供统一的加载状态管理。
- Fragment 支持:允许组件模板拥有多个根节点。
- 响应式系统重构:基于
Proxy构建,解决了 Vue 2 中Object.defineProperty带来的新增属性监听、数组索引变更等限制。
Composition API
基本概念
Composition API 是 Vue 3 引入的一组基于函数的 API,用于描述组件逻辑。与 Vue 2 的 Options API 相比,逻辑按照关注点组织为独立函数,而不是分散在 data、methods、watch 等选项区块中。
基本用法
一个简单的计数器,在 Vue 2 中使用 Options API 写作:
js
export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
},
watch: {
count(newVal) {
console.log('count changed:', newVal);
},
},
};在 Vue 3 中使用 Composition API 实现相同功能:
js
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
function increment() {
count.value++;
}
watch(count, (newVal) => {
console.log('count changed:', newVal);
});
return { count, increment };
},
};在模板中访问 count 时不需要 .value,因为 ref 在模板上下文中会自动解包。
逻辑抽离与复用
把计数逻辑抽取成一个可复用的函数(composable):
js
import { ref } from 'vue';
function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}在任意组件中使用:
js
const { count, increment } = useCounter();同一个 useCounter 可以在多个组件中调用,每个组件内都会获得独立的响应式状态。
与 Options API 的区别
- 同一功能的数据、方法、侦听等可以写在一个 composable 中,避免在多个选项间切换。
- 复用不再强依赖 mixin 或高阶组件,组合函数可以实现更细粒度的逻辑复用。
- 函数参数和返回值类型推断路径更短,TypeScript 类型检查体验更好。
setup 与 <script setup>
setup 函数
setup() 是 Composition API 的入口点。它在组件实例初始化时、beforeCreate 之前调用,用于声明响应式状态、计算属性、侦听器以及各类回调。
基本写法:
js
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
return { count };
},
mounted() {
console.log(this.count); // 0
},
};在 setup() 中不能访问 this,此时组件实例尚未完成创建,上下文由 setup() 的参数提供。
参数
setup(props, context) 接收两个参数:
props:当前组件接收的属性对象,具有响应性,会随外部传入的新值同步更新。context:包含透传属性attrs、插槽slots、触发事件emit以及用于暴露公共方法的expose。
具体类型:
js
export default {
setup(props, context) {
// 透传 Attribute(等价于 $attrs)
console.log(context.attrs);
// 插槽(等价于 $slots)
console.log(context.slots);
// 触发事件(等价于 $emit)
console.log(context.emit);
// 暴露到模板引用的方法/属性
console.log(context.expose);
},
};注意点
- 在
setup()中使用this会得到undefined,此时组件实例尚未完成创建。 setup()的返回值应当是同步对象。只有当组件位于<Suspense>内部时,才允许async setup()返回异步的数据结构。
<script setup> 语法糖
<script setup> 是 setup() 的编译期语法糖,可以在单文件组件中直接使用 Composition API 样式编写逻辑,无需显式定义并返回对象。
html
<script setup>
import { ref } from 'vue';
const msg = 'Hello!';
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
</template>上面的代码等价于:
html
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const msg = 'Hello!';
const count = ref(0);
function increment() {
count.value++;
}
return { msg, count, increment };
},
});
</script><script setup> 中声明的顶级变量和函数会直接暴露给模板使用。相较于普通 <script>,它的区别在于:
- 减少了模板与逻辑间的样板代码。
- Props 和自定义事件可通过编译器宏(如
defineProps、defineEmits)与 TypeScript 直接集成。 - 模板编译到同一作用域,渲染函数的上下文代理开销更低。
- IDE 对类型和引用的推导路径更简洁。
Teleport
基本概念
<Teleport> 是 Vue 3 内置组件,可以将子组件的某段模板内容“传送”到当前 DOM 结构以外的指定容器中。传送后,组件内部的响应式逻辑、事件监听仍保持原来的父子关系。
典型场景
- 模态框、对话框
- 全屏遮罩层
- 下拉菜单或悬浮提示
- 需要避开父元素
overflow: hidden或样式隔离的内容
在这类场景中,UI 元素在视觉和可访问性上需要脱离当前组件所在的 DOM 层级,避免受到上层 CSS 或布局限制。
基本用法
以下示例将模态框内容传送到 <body> 下的一个 <div id="modal-container"> 中:
html
<template>
<button @click="show = true">打开对话框</button>
<Teleport to="#modal-container" :disabled="!show">
<div class="modal">
<p>这是一个传送出去的对话框。</p>
<button @click="show = false">关闭</button>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue';
const show = ref(false);
</script>被 <Teleport> 包裹的 DOM 会渲染到 to 属性指定的目标容器中,但组件自身的状态(show)仍由原组件维护。
Props
| 属性名 | 类型 | 说明 |
|---|---|---|
to | string | HTMLElement | 必填,传送的目标。可以是 CSS 选择器字符串或 DOM 元素对象。 |
disabled | boolean | 是否禁用传送。true 时,内容渲染在原位置而非目标容器。默认为 false。 |
工作原理
Vue 会给 Teleport 生成的虚拟节点打上特殊符号标记:
ts
export const TELEPORT: unique symbol = Symbol(__DEV__ ? `Teleport` : ``);渲染器在 patch 阶段识别到该标记后,会执行专门的传送逻辑,绕过正常 DOM 挂载路径,将对应 DOM 插入到目标容器中。
注意点
- 被传送的元素仍然属于原先的组件作用域,响应式绑定、事件处理都不受影响。
- 如果目标容器尚未挂载(例如组件先于 DOM 渲染),传送会失败,需要确保目标节点存在。
- 由于元素已经离开了原始位置,依赖原位置 CSS 布局的做法会失效。
disabled属性可以在运行时动态切换,用于条件性地保持原位渲染。
Suspense
基本概念
<Suspense> 用于协调组件树中的异步依赖。它可以等待下一个层级内多个异步子组件(或多个异步操作)全部就绪,在等待期间显示备用内容(fallback)。Suspense 主要面向异步组件和顶层 async setup() 的场景。
基本用法
定义一个异步子组件,该组件在 setup() 中返回一个 Promise:
js
// AsyncChild.vue
export default {
async setup() {
// 模拟数据请求
const data = await fetch('/api/data').then((res) => res.json());
return { data };
},
};在父组件中使用 <Suspense> 包裹:
html
<template>
<Suspense>
<template #default>
<AsyncChild />
</template>
<template #fallback>
<p>加载中…</p>
</template>
</Suspense>
</template>
<script setup>
import AsyncChild from './AsyncChild.vue';
</script>渲染过程如下:
<Suspense>渲染default插槽中的组件。- 发现
AsyncChild的setup()返回了一个异步任务,将其加入等待队列。 - 由于存在未完成的异步依赖,暂时展示
fallback插槽的内容(加载提示)。 - 所有异步依赖完成后,卸载 fallback 内容,挂载
default插槽的实际结果。
渲染流程
从编译和运行时的角度,可以拆分成几个阶段:
- 编译阶段:
<Suspense>会被编译为以SuspenseImpl为类型的 VNode,其子内容被分别映射为slots.default和slots.fallback。 - 执行 default 插槽:运行时尝试执行
default中所有组件的setup()。当某个setup()中存在await或返回 Promise,Vue 会将其收集到suspense.asyncDeps队列。 - 渲染 fallback:
asyncDeps计数大于 0 时,渲染器优先渲染fallback插槽的内容。 - 完成后切换:每一个异步依赖 resolve 后会减少计数。当所有依赖都完成,Vue 会卸载 fallback DOM,并挂载
default插槽的真实内容。
注意点
<Suspense>只处理其直接子组件树中的异步依赖。对于嵌套更深的异步请求,需要自行管理加载状态。- 如果
setup()是同步的或组件不使用异步操作,<Suspense>会立即渲染default内容,不显示 fallback。 - 异步依赖包括
async setup()、使用defineAsyncComponent加载的异步组件、以及在setup中显式返回的 Promise(较少使用)。 - 在 SSR(服务端渲染)场景下,
<Suspense>同样可用于控制水合阶段的加载状态。
Fragment
基本概念
Vue 2 要求组件模板必须有唯一根节点。Vue 3 去掉了这一限制,允许组件返回多个根元素。运行时内部会将这多个根元素封装在一个 Fragment 虚拟节点中。
基本用法
在 Vue 3 的单文件组件中直接编写多个同级根元素:
html
<template>
<header>Header</header>
<main>Content</main>
<footer>Footer</footer>
</template>编译后的虚拟节点树会被包裹在一个 Fragment 节点中,该节点本身不会生成真实 DOM,仅作为逻辑容器存在。
注意点
- 多根节点组件中,需要显式指定哪些根元素接收透传的
class、style和$attrs。可以通过v-bind="$attrs"手动绑定。 - 每个同级根节点应当设置唯一的
key(尤其在循环渲染内部使用时),否则 Vue 无法有效区分同级元素的变动。 - Fragment 减轻了模板对无意义包裹元素(如
<div>)的依赖,减少了 DOM 嵌套层级。
响应系统重构
工作原理
Vue 2 基于 Object.defineProperty 实现响应式,递归地将对象属性转换为 getter/setter。这导致几个结构性限制:无法检测新属性的添加、删除,以及基于索引修改数组元素。Vue 3 采用 Proxy 重写了响应式内核,拦截层级改为对象整体,使得上述操作都成为可能。
| Vue 版本 | 响应式实现方式 |
|---|---|
| Vue 2 | Object.defineProperty(递归劫持,新增/删除属性需借助 Vue.set/Vue.delete) |
| Vue 3 | Proxy(整体代理,动态拦截、无需递归初始化时遍历全部属性,支持新增/删除) |
当通过 ref 或 reactive 创建响应式数据时,Proxy 会拦截读取和写入操作。Vue 内部的 effect 系统在执行副作用函数时通过 track 收集依赖,在数据变更时通过 trigger 通知更新。
API
reactive()
将普通对象(或数组)转换为深层响应式代理对象。修改属性的值、新增属性、删除属性都会被拦截并触发更新。
js
import { reactive } from 'vue';
const state = reactive({
count: 0,
nested: {
label: 'deep',
},
});
state.count++; // 触发更新
state.nested.label = 'updated'; // 深层属性也触发更新
state.newKey = 'new value'; // 新属性也会被代理注意事项:
reactive()返回的是原始对象的代理,全等比较为true的只有代理本身。- 对响应式对象整体解构会失去响应性,因为解构得到的是值拷贝而非引用。例如
const { count } = state之后count不再与state保持响应式关联。如果需要解构,可使用toRefs()将属性转为ref。
ref()
接受一个内部值(可以是基本类型或对象),返回一个响应式的、可变的引用对象。该对象仅有一个 .value 属性指向内部值。
js
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;在模板中访问时 ref 会自动解包,不需要 .value。当 ref 作为数组或原生集合的元素时,解包不会发生,需要使用 .value。
注意事项:
- 当值是一个对象时,内部会调用
reactive()对其进行深层代理。 ref可以传递引用,将单个响应式值从组件中导出而不会被丢失响应性,常用于组合式 API 的返回值。
computed()
创建一个基于其他响应式状态的计算属性。计算属性的值在依赖未改变时会被缓存,只有当依赖变化时才会重新求值。
js
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
console.log(fullName.value); // "John Doe"computed 默认返回只读的引用对象,也可通过提供具有 get 和 set 的函数创建可写的计算属性。
watch()
用于观察一个或多个响应式数据源,在数据源变化时执行回调。
js
import { ref, watch } from 'vue';
const searchQuery = ref('');
watch(searchQuery, (newValue, oldValue) => {
console.log(`搜索词改变了:${oldValue} -> ${newValue}`);
});可以观察的对象包括 ref、reactive 对象、getter 函数或多个数据源组成的数组。当观察 reactive 对象时,默认是深层观察,无法获取旧值;若观察 ref 或 getter,可通过参数拿到前一个值。
effect()
effect() 是 Vue 响应系统最底层的 API。它会执行传入的函数,并在执行过程中自动追踪其中访问过的所有响应式依赖,当任一依赖变化时会重新执行该函数。
js
import { ref, effect } from 'vue';
const count = ref(0);
effect(() => {
console.log(`count 的最新值:${count.value}`);
});
// 输出:"count 的最新值:0"
count.value++;
// 输出:"count 的最新值:1"effect() 主要用于构建更上层的 API(如 watch、computed 内部都依赖它),直接在应用代码中使用的场景相对较少。
应用
- 适用结构化对象需要整体响应式的场景,使用
reactive()。 - 适用独立基本类型值,或需要将响应式值从组合函数中返回并保持响应性的场景,使用
ref()。 - 基于已有状态派生新值,且希望自动缓存时,使用
computed()。 - 需要在状态变化时执行副作用(例如请求数据、操作 DOM),使用
watch()。 - 构建自定义的响应式工具时,才会用到
effect()。
