Skip to content

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 = axios

emits 选项 ​

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
}

破坏性变更总结 ​

变更Vue2Vue3
实例创建new Vue()createApp()
全局 APIVue.prototype.xxxapp.config.globalProperties.xxx
生命周期beforeDestroy / destroyedbeforeUnmount / 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 / updatemounted / updated / unmounted