Vue3 新特性与 Script Setup
script setup 语法糖
<script setup> 是在单文件组件中使用组合式 API 的编译时语法糖,简化了大量样板代码。
基本用法
vue
<script setup>
import { ref } from 'vue'
// 变量和函数直接用于模板
const count = ref(0)
const message = 'Hello'
function increment() {
count.value++
}
</script>
<template>
<p>{{ message }}</p>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</template><script setup>
import { ref } from 'vue'
// 变量和函数直接用于模板
const count = ref(0)
const message = 'Hello'
function increment() {
count.value++
}
</script>
<template>
<p>{{ message }}</p>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</template>defineProps / defineEmits
vue
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
user: Object
})
const emit = defineEmits(['update', 'delete'])
function handleUpdate() {
emit('update', { id: props.user?.id })
}
</script>
<template>
<h3>{{ props.title }}</h3>
<p>数量:{{ props.count }}</p>
<button @click="handleUpdate">更新</button>
</template><script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
user: Object
})
const emit = defineEmits(['update', 'delete'])
function handleUpdate() {
emit('update', { id: props.user?.id })
}
</script>
<template>
<h3>{{ props.title }}</h3>
<p>数量:{{ props.count }}</p>
<button @click="handleUpdate">更新</button>
</template>仅限 TypeScript 的类型声明方式
vue
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface User { name: string; age: number }
const props = defineProps<{
title: string
count?: number
user: User
}>()
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', id: number): void
}>()
// withDefaults — 声明默认值
const props2 = withDefaults(defineProps<{
title: string
count?: number
list?: string[]
}>(), {
count: 0,
list: () => ['default']
})
</script><script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface User { name: string; age: number }
const props = defineProps<{
title: string
count?: number
user: User
}>()
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', id: number): void
}>()
// withDefaults — 声明默认值
const props2 = withDefaults(defineProps<{
title: string
count?: number
list?: string[]
}>(), {
count: 0,
list: () => ['default']
})
</script>defineExpose
<script setup> 中的变量默认不暴露给父组件,通过 defineExpose 控制。
vue
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const reset = () => count.value = 0
defineExpose({ count, reset })
</script><!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const reset = () => count.value = 0
defineExpose({ count, reset })
</script>vue
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
const childRef = ref()
function resetChild() {
childRef.value.reset()
console.log(childRef.value.count)
}
</script>
<template>
<Child ref="childRef" />
</template><!-- 父组件 -->
<script setup>
import { ref } from 'vue'
const childRef = ref()
function resetChild() {
childRef.value.reset()
console.log(childRef.value.count)
}
</script>
<template>
<Child ref="childRef" />
</template>defineOptions
在 <script setup> 中设置组件选项。
vue
<script setup>
defineOptions({
name: 'MyComponent',
inheritAttrs: false
})
</script><script setup>
defineOptions({
name: 'MyComponent',
inheritAttrs: false
})
</script>defineModel — 双向绑定宏
在 Vue 3.3+ 中,defineModel 简化了 v-model 的组件内实现。
vue
<!-- 子组件 -->
<script setup>
const model = defineModel({ type: String, default: '' })
// 等价于:
// const props = defineProps({ modelValue: String })
// const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input v-model="model" />
</template><!-- 子组件 -->
<script setup>
const model = defineModel({ type: String, default: '' })
// 等价于:
// const props = defineProps({ modelValue: String })
// const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input v-model="model" />
</template>vue
<!-- 父组件 -->
<template>
<CustomInput v-model="search" />
</template><!-- 父组件 -->
<template>
<CustomInput v-model="search" />
</template>带参数的 v-model
vue
<!-- 子组件 -->
<script setup>
const title = defineModel('title', { required: true })
</script>
<template>
<input v-model="title" />
</template><!-- 子组件 -->
<script setup>
const title = defineModel('title', { required: true })
</script>
<template>
<input v-model="title" />
</template>vue
<!-- 父组件 -->
<template>
<CustomForm v-model:title="pageTitle" />
</template><!-- 父组件 -->
<template>
<CustomForm v-model:title="pageTitle" />
</template>自定义修饰符
vue
<script setup>
const [model, modifiers] = defineModel({
set(value) {
// 自定义 setter
if (modifiers.value.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script><script setup>
const [model, modifiers] = defineModel({
set(value) {
// 自定义 setter
if (modifiers.value.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>Teleport 传送门
将组件内容渲染到 DOM 的指定位置(如 body)。
vue
<template>
<button @click="open = true">打开模态框</button>
<!-- to 属性指定渲染位置 -->
<teleport to="body">
<div v-if="open" class="modal">
<p>模态框内容,渲染在 body 下</p>
<button @click="open = false">关闭</button>
</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script><template>
<button @click="open = true">打开模态框</button>
<!-- to 属性指定渲染位置 -->
<teleport to="body">
<div v-if="open" class="modal">
<p>模态框内容,渲染在 body 下</p>
<button @click="open = false">关闭</button>
</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>Teleport 多个使用
多个 <teleport> 渲染到同一目标时,按顺序追加。
vue
<template>
<teleport to="#modals">
<div>模态框1</div>
</teleport>
<teleport to="#modals">
<div>模态框2</div>
</teleport>
</template><template>
<teleport to="#modals">
<div>模态框1</div>
</teleport>
<teleport to="#modals">
<div>模态框2</div>
</teleport>
</template>禁用 Teleport
vue
<template>
<teleport to="body" :disabled="isMobile">
<div>移动端不传送</div>
</teleport>
</template><template>
<teleport to="body" :disabled="isMobile">
<div>移动端不传送</div>
</teleport>
</template>Suspense 异步组件
<Suspense> 用于处理异步依赖,在等待异步组件时显示 fallback 内容。
vue
<template>
<Suspense>
<!-- 异步组件加载完成后显示 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中显示 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
</script><template>
<Suspense>
<!-- 异步组件加载完成后显示 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中显示 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
</script>异步组件的进阶配置
js
import { defineAsyncComponent } from 'vue'
import Loading from './Loading.vue'
import Error from './Error.vue'
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: Loading, // 加载中组件
errorComponent: Error, // 加载失败组件
delay: 200, // 延迟显示 loading(防闪烁)
timeout: 3000, // 超时时间
suspensible: false, // 是否可被 Suspense 挂起
onError(error, retry, fail, attempts) {
if (attempts < 3) {
retry() // 重试
} else {
fail()
}
}
})import { defineAsyncComponent } from 'vue'
import Loading from './Loading.vue'
import Error from './Error.vue'
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: Loading, // 加载中组件
errorComponent: Error, // 加载失败组件
delay: 200, // 延迟显示 loading(防闪烁)
timeout: 3000, // 超时时间
suspensible: false, // 是否可被 Suspense 挂起
onError(error, retry, fail, attempts) {
if (attempts < 3) {
retry() // 重试
} else {
fail()
}
}
})script setup 中的顶层 await
vue
<!-- 直接在 script setup 中使用 await -->
<script setup>
const { data } = await fetch('/api/user').then(r => r.json())
// 组件会等待数据加载完成后才渲染
</script>
<template>
<p>用户:{{ data.name }}</p>
</template><!-- 直接在 script setup 中使用 await -->
<script setup>
const { data } = await fetch('/api/user').then(r => r.json())
// 组件会等待数据加载完成后才渲染
</script>
<template>
<p>用户:{{ data.name }}</p>
</template>注意: 顶层 await 会自动让组件变为一个异步依赖,需要配合 <Suspense> 使用。
Fragment 片段
Vue3 支持组件拥有多个根节点。
vue
<!-- Vue2 — 必须有一个根节点 -->
<template>
<div>
<header>...</header>
<main>...</main>
</div>
</template>
<!-- Vue3 — 支持多个根节点 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template><!-- Vue2 — 必须有一个根节点 -->
<template>
<div>
<header>...</header>
<main>...</main>
</div>
</template>
<!-- Vue3 — 支持多个根节点 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>v-model 升级
Vue3 的 v-model 有了重大改进,支持多个 v-model 和自定义修饰符。
vue
<!-- 组件上多个 v-model -->
<CustomForm
v-model:title="pageTitle"
v-model:content="pageContent"
v-model:author="author"
/><!-- 组件上多个 v-model -->
<CustomForm
v-model:title="pageTitle"
v-model:content="pageContent"
v-model:author="author"
/>vue
<!-- 子组件实现 -->
<script setup>
const title = defineModel('title')
const content = defineModel('content')
</script>
<template>
<input v-model="title" />
<textarea v-model="content" />
</template><!-- 子组件实现 -->
<script setup>
const title = defineModel('title')
const content = defineModel('content')
</script>
<template>
<input v-model="title" />
<textarea v-model="content" />
</template>新指令:v-memo
Vue 3.2+ 新增,用于优化大型列表的渲染性能。
vue
<template>
<!-- 仅在 value 和 name 变化时重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.value, item.name]">
<p>{{ item.value }} - {{ item.name }}</p>
<p>{{ item.other }}</p>
</div>
</template><template>
<!-- 仅在 value 和 name 变化时重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.value, item.name]">
<p>{{ item.value }} - {{ item.name }}</p>
<p>{{ item.other }}</p>
</div>
</template>注意: v-memo 应用于 v-for 时,两者必须绑定在同一个元素上。
CSS 新特性
状态驱动的 CSS(v-bind in CSS)
vue
<script setup>
const color = ref('red')
const theme = { fontSize: '20px' }
</script>
<style scoped>
.text {
color: v-bind(color); // 绑定 ref
font-size: v-bind('theme.fontSize'); // 绑定对象属性
}
</style><script setup>
const color = ref('red')
const theme = { fontSize: '20px' }
</script>
<style scoped>
.text {
color: v-bind(color); // 绑定 ref
font-size: v-bind('theme.fontSize'); // 绑定对象属性
}
</style>CSS Modules
vue
<template>
<div :class="$style.red">红色文字</div>
</template>
<style module>
.red { color: red; }
</style>
<style module="classes">
.bold { font-weight: bold; }
</style><template>
<div :class="$style.red">红色文字</div>
</template>
<style module>
.red { color: red; }
</style>
<style module="classes">
.bold { font-weight: bold; }
</style>vue
<script setup>
import { useCssModule } from 'vue'
// 获取 CSS Modules
const style = useCssModule() // 默认模块
const classes = useCssModule('classes') // 命名模块
</script><script setup>
import { useCssModule } from 'vue'
// 获取 CSS Modules
const style = useCssModule() // 默认模块
const classes = useCssModule('classes') // 命名模块
</script>深度选择器
vue
<style scoped>
/* 深度选择器 */
:deep(.foo) { color: red; }
/* 插槽选择器 */
:slotted(div) { color: blue; }
/* 全局选择器 */
:global(.red) { background: red; }
/* Vue2 写法兼容 */
::v-deep(.foo) { }
::v-slotted(.foo) { }
::v-global(.foo) { }
</style><style scoped>
/* 深度选择器 */
:deep(.foo) { color: red; }
/* 插槽选择器 */
:slotted(div) { color: blue; }
/* 全局选择器 */
:global(.red) { background: red; }
/* Vue2 写法兼容 */
::v-deep(.foo) { }
::v-slotted(.foo) { }
::v-global(.foo) { }
</style>其他新特性
全局 API 变化
js
// Vue2 — 全局 API 挂在 Vue 对象上
Vue.component('my-component', {})
Vue.directive('my-directive', {})
Vue.mixin({})
Vue.prototype.$http = axios
// Vue3 — 应用实例 API
const app = createApp(App)
app.component('my-component', {})
app.directive('my-directive', {})
app.mixin({})
app.config.globalProperties.$http = axios// Vue2 — 全局 API 挂在 Vue 对象上
Vue.component('my-component', {})
Vue.directive('my-directive', {})
Vue.mixin({})
Vue.prototype.$http = axios
// Vue3 — 应用实例 API
const app = createApp(App)
app.component('my-component', {})
app.directive('my-directive', {})
app.mixin({})
app.config.globalProperties.$http = axiosemits 选项
Vue3 推荐显式声明组件可触发的事件。
vue
<script setup>
const emit = defineEmits({
// 对象语法 — 带校验
submit: (payload) => {
if (payload.email && payload.password) return true
console.warn('submit 事件参数无效')
return false
},
// 简单声明
close: null
})
</script><script setup>
const emit = defineEmits({
// 对象语法 — 带校验
submit: (payload) => {
if (payload.email && payload.password) return true
console.warn('submit 事件参数无效')
return false
},
// 简单声明
close: null
})
</script>异步组件
js
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './Error.vue'
import LoadingComponent from './Loading.vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})import { defineAsyncComponent } from 'vue'
import ErrorComponent from './Error.vue'
import LoadingComponent from './Loading.vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})effectScope — 作用域 API
用于批量控制 effect(watch、computed 等)的停用。
js
import { effectScope, computed, watch, onScopeDispose } from 'vue'
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => count.value * 2)
watch(doubled, () => console.log(doubled.value))
// 注册清理回调
onScopeDispose(() => {
console.log('scope 已清理')
})
})
// 停止 scope 内所有 effect
scope.stop()import { effectScope, computed, watch, onScopeDispose } from 'vue'
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => count.value * 2)
watch(doubled, () => console.log(doubled.value))
// 注册清理回调
onScopeDispose(() => {
console.log('scope 已清理')
})
})
// 停止 scope 内所有 effect
scope.stop()getCurrentInstance
js
import { getCurrentInstance } from 'vue'
function useGlobalConfig() {
const instance = getCurrentInstance()
return instance?.appContext.config.globalProperties
}import { getCurrentInstance } from 'vue'
function useGlobalConfig() {
const instance = getCurrentInstance()
return instance?.appContext.config.globalProperties
}破坏性变更总结
| 变更 | Vue2 | Vue3 |
|---|---|---|
| 实例创建 | new Vue() | createApp() |
| 全局 API | Vue.prototype.xxx | app.config.globalProperties.xxx |
| 生命周期 | beforeDestroy / destroyed | beforeUnmount / unmounted |
| 过滤器 | filters: {} | 已移除,用 computed 替代 |
| eventBus | $on / $off / $once | 已移除,用 mitt 等库替代 |
| 函数式组件 | functional: true | 普通函数即可 |
| v-model | :value + @input | :modelValue + @update:modelValue |
.sync 修饰符 | :title.sync | 用 v-model:title 替代 |
| key 使用 | v-for 时 key 放哪都可以 | 必须放在 v-if / v-for 同一元素 |
| 根节点 | 必须有且仅有一个 | 支持多根节点(Fragment) |
| 自定义指令钩子 | bind / inserted / update | mounted / updated / unmounted |
Cao Note