Vue3 组合式 API
setup 函数
组合式 API 的入口点,在组件创建之前执行。
vue
<script>
import { ref, reactive } from 'vue'
export default {
setup(props, context) {
// props — 响应式,不可解构
// context — { attrs, slots, emit, expose }
const count = ref(0)
function increment() {
count.value++
}
// 暴露到模板
return { count, increment }
}
}
</script><script>
import { ref, reactive } from 'vue'
export default {
setup(props, context) {
// props — 响应式,不可解构
// context — { attrs, slots, emit, expose }
const count = ref(0)
function increment() {
count.value++
}
// 暴露到模板
return { count, increment }
}
}
</script>注意:
props不能解构,否则失去响应式context可以解构:{ attrs, slots, emit, expose }setup中不能访问thissetup中可以使用async/await(但组件需要配合 Suspense 或自行处理 loading)
响应式核心
ref — 基本类型的响应式
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
function increment() {
count.value++ // template 中自动解包,无需 .value
}
console.log(count.value) // 0
</script>
<template>
<p>{{ count }}</p> <!-- 自动解包 -->
<p>{{ message }}</p>
<button @click="increment">+1</button>
</template><script setup>
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
function increment() {
count.value++ // template 中自动解包,无需 .value
}
console.log(count.value) // 0
</script>
<template>
<p>{{ count }}</p> <!-- 自动解包 -->
<p>{{ message }}</p>
<button @click="increment">+1</button>
</template>ref 接收对象时会自动调用 reactive 处理。
reactive — 对象响应式
vue
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: '张三', age: 18 }
})
function update() {
state.count++
state.user.name = '李四'
}
// reactive 支持的操作
state.newKey = '自动响应式' // Vue3 中新增属性也是响应式的
delete state.user // 删除属性也是响应式的
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.user.name }}</p>
</template><script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: '张三', age: 18 }
})
function update() {
state.count++
state.user.name = '李四'
}
// reactive 支持的操作
state.newKey = '自动响应式' // Vue3 中新增属性也是响应式的
delete state.user // 删除属性也是响应式的
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.user.name }}</p>
</template>ref vs reactive
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 任意类型(基本类型+对象) | 对象/数组 |
| 访问方式 | .value(模板中自动解包) | 直接访问 |
| 重新赋值 | 支持 count = ref(1) | 不支持直接替换整个对象 |
| 适用场景 | 基本类型、需要重新赋值的变量 | 复杂对象、表单数据 |
js
const arr = reactive([1, 2, 3])
// 支持索引修改
arr[0] = 10
// 支持新增
arr[3] = 4const arr = reactive([1, 2, 3])
// 支持索引修改
arr[0] = 10
// 支持新增
arr[3] = 4其他响应式 API
readonly
js
import { readonly, reactive } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
copy.count++ // 警告:不可修改import { readonly, reactive } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
copy.count++ // 警告:不可修改shallowRef / shallowReactive
js
import { shallowRef, shallowReactive } from 'vue'
// 浅层响应式:只追踪 .value 变化,对象内部变化不触发
const state = shallowRef({ count: 0 })
state.value.count++ // 不触发更新
state.value = { count: 1 } // 触发更新
// 浅层响应式对象
const obj = shallowReactive({ nested: { count: 0 } })
obj.nested.count++ // 不触发更新import { shallowRef, shallowReactive } from 'vue'
// 浅层响应式:只追踪 .value 变化,对象内部变化不触发
const state = shallowRef({ count: 0 })
state.value.count++ // 不触发更新
state.value = { count: 1 } // 触发更新
// 浅层响应式对象
const obj = shallowReactive({ nested: { count: 0 } })
obj.nested.count++ // 不触发更新triggerRef
js
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0 })
function update() {
state.value.count++
triggerRef(state) // 手动触发依赖更新
}import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0 })
function update() {
state.value.count++
triggerRef(state) // 手动触发依赖更新
}toRef / toRefs
js
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({ foo: 1, bar: 2 })
// 将对象的单个属性转为 ref
const fooRef = toRef(state, 'foo')
fooRef.value++ // state.foo 也会变为 2
// 将整个对象转为 refs(解构时保持响应式)
const { foo, bar } = toRefs(state)
// foo 和 bar 都是 ref,解构后仍然响应式import { reactive, toRef, toRefs } from 'vue'
const state = reactive({ foo: 1, bar: 2 })
// 将对象的单个属性转为 ref
const fooRef = toRef(state, 'foo')
fooRef.value++ // state.foo 也会变为 2
// 将整个对象转为 refs(解构时保持响应式)
const { foo, bar } = toRefs(state)
// foo 和 bar 都是 ref,解构后仍然响应式unref / isRef
js
import { ref, unref, isRef } from 'vue'
const count = ref(0)
console.log(unref(count)) // 0(相当于 isRef(val) ? val.value : val)
console.log(isRef(count)) // trueimport { ref, unref, isRef } from 'vue'
const count = ref(0)
console.log(unref(count)) // 0(相当于 isRef(val) ? val.value : val)
console.log(isRef(count)) // truecustomRef
创建自定义的 ref,显式控制依赖追踪和触发更新。
vue
<script setup>
import { customRef } from 'vue'
function useDebouncedRef(value, delay = 300) {
let timer
return customRef((track, trigger) => {
return {
get() {
track() // 追踪依赖
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger() // 触发更新
}, delay)
}
}
})
}
const text = useDebouncedRef('hello', 500)
</script>
<template>
<input v-model="text" />
</template><script setup>
import { customRef } from 'vue'
function useDebouncedRef(value, delay = 300) {
let timer
return customRef((track, trigger) => {
return {
get() {
track() // 追踪依赖
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger() // 触发更新
}, delay)
}
}
})
}
const text = useDebouncedRef('hello', 500)
</script>
<template>
<input v-model="text" />
</template>计算属性
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
// 只读计算属性
const double = computed(() => count.value * 2)
// 可读写计算属性
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
// 调试计算属性(仅开发模式)
const debugDouble = computed(() => count.value * 2, {
onTrack(e) {
console.log('追踪依赖:', e)
},
onTrigger(e) {
console.log('触发更新:', e)
}
})
</script><script setup>
import { ref, computed } from 'vue'
const count = ref(1)
// 只读计算属性
const double = computed(() => count.value * 2)
// 可读写计算属性
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
// 调试计算属性(仅开发模式)
const debugDouble = computed(() => count.value * 2, {
onTrack(e) {
console.log('追踪依赖:', e)
},
onTrigger(e) {
console.log('触发更新:', e)
}
})
</script>侦听器
watch
vue
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: '张三', age: 18 })
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// 监听 reactive 的某个属性(需要 getter 函数)
watch(() => state.age, (newVal, oldVal) => {
console.log(`age: ${oldVal} → ${newVal}`)
})
// 监听 reactive 整个对象(默认开启 deep)
watch(state, (newVal, oldVal) => {
console.log('state 变化了', newVal)
})
// 监听多个源
watch([count, () => state.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${newCount}, age: ${newAge}`)
})
// 配置选项
watch(count, (newVal, oldVal) => {
console.log(newVal)
}, {
immediate: true, // 初始化时立即执行
deep: true, // 深度监听(监听 ref 对象时)
flush: 'post' // 'pre' | 'post' | 'sync',DOM 更新后触发回调
})
</script><script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: '张三', age: 18 })
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// 监听 reactive 的某个属性(需要 getter 函数)
watch(() => state.age, (newVal, oldVal) => {
console.log(`age: ${oldVal} → ${newVal}`)
})
// 监听 reactive 整个对象(默认开启 deep)
watch(state, (newVal, oldVal) => {
console.log('state 变化了', newVal)
})
// 监听多个源
watch([count, () => state.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${newCount}, age: ${newAge}`)
})
// 配置选项
watch(count, (newVal, oldVal) => {
console.log(newVal)
}, {
immediate: true, // 初始化时立即执行
deep: true, // 深度监听(监听 ref 对象时)
flush: 'post' // 'pre' | 'post' | 'sync',DOM 更新后触发回调
})
</script>watchEffect
自动追踪回调中使用的响应式依赖,无需手动指定监听源。
vue
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('张三')
// 自动追踪 count 和 name
const stop = watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
})
// 停止监听
stop()
// 清除副作用(如取消请求)
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('数据已保存')
}, 1000)
onCleanup(() => {
clearTimeout(timer) // 组件卸载或依赖变化时清除
})
})
// 配置 flush: 'post' 可以在 DOM 更新后执行
watchEffect(() => {
console.log(document.querySelector('.msg')?.textContent)
}, { flush: 'post' })
</script><script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('张三')
// 自动追踪 count 和 name
const stop = watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
})
// 停止监听
stop()
// 清除副作用(如取消请求)
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('数据已保存')
}, 1000)
onCleanup(() => {
clearTimeout(timer) // 组件卸载或依赖变化时清除
})
})
// 配置 flush: 'post' 可以在 DOM 更新后执行
watchEffect(() => {
console.log(document.querySelector('.msg')?.textContent)
}, { flush: 'post' })
</script>watch vs watchEffect
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式指定监听源 | 自动追踪 |
| 旧值 | 可以访问旧值 | 不能 |
| 懒执行 | 默认懒执行(除非 immediate: true) | 立即执行 |
| 适用场景 | 需要旧值、特定数据变化时 | 不需要旧值、自动追踪依赖 |
生命周期钩子
在 setup 中通过 onX 形式使用生命周期钩子。
| 选项式 API | 组合式 API | 说明 |
|---|---|---|
beforeCreate | — | setup 本身就是 |
created | — | setup 本身就是 |
beforeMount | onBeforeMount | 挂载前 |
mounted | onMounted | 已挂载 |
beforeUpdate | onBeforeUpdate | 更新前 |
updated | onUpdated | 已更新 |
beforeUnmount | onBeforeUnmount | 卸载前 |
unmounted | onUnmounted | 已卸载 |
errorCaptured | onErrorCaptured | 错误捕获 |
renderTracked | onRenderTracked | 调试:追踪响应式依赖 |
renderTriggered | onRenderTriggered | 调试:触发重新渲染 |
activated | onActivated | keep-alive 激活 |
deactivated | onDeactivated | keep-alive 停用 |
vue
<script setup>
import { onMounted, onBeforeUnmount, onErrorCaptured } from 'vue'
onMounted(() => {
console.log('组件已挂载')
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
window.removeEventListener('resize', handleResize)
})
onErrorCaptured((err) => {
console.error('捕获错误:', err)
return false // 阻止错误继续传播
})
</script><script setup>
import { onMounted, onBeforeUnmount, onErrorCaptured } from 'vue'
onMounted(() => {
console.log('组件已挂载')
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
window.removeEventListener('resize', handleResize)
})
onErrorCaptured((err) => {
console.error('捕获错误:', err)
return false // 阻止错误继续传播
})
</script>Provide / Inject
vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'
const count = ref(0)
const updateCount = () => count.value++
// 使用 readonly 防止子组件修改
provide('count', readonly(count))
provide('updateCount', updateCount)
// provide 也支持 Symbol 作为 key
</script><!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'
const count = ref(0)
const updateCount = () => count.value++
// 使用 readonly 防止子组件修改
provide('count', readonly(count))
provide('updateCount', updateCount)
// provide 也支持 Symbol 作为 key
</script>vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount')
// 设置默认值
const theme = inject('theme', 'light')
// 使用工厂函数
const config = inject('config', () => ({ lang: 'zh' }), true) // true 表示工厂函数
</script><!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount')
// 设置默认值
const theme = inject('theme', 'light')
// 使用工厂函数
const config = inject('config', () => ({ lang: 'zh' }), true) // true 表示工厂函数
</script>组合式函数 (Composables)
组合式函数是利用 Vue 组合式 API 封装和复用逻辑的函数。
基本示例
js
// composables/useMouse.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onBeforeUnmount(() => window.removeEventListener('mousemove', update))
return { x, y }
}// composables/useMouse.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onBeforeUnmount(() => window.removeEventListener('mousemove', update))
return { x, y }
}vue
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置:{{ x }}, {{ y }}</p>
</template><script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置:{{ x }}, {{ y }}</p>
</template>带参数的组合式函数
js
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(source, delay = 300) {
const debouncedValue = ref(source.value)
watch(source, (newVal) => {
const timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(source, delay = 300) {
const debouncedValue = ref(source.value)
watch(source, (newVal) => {
const timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}vue
<script setup>
import { ref } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
const search = ref('')
const debouncedSearch = useDebounce(search, 500)
</script><script setup>
import { ref } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
const search = ref('')
const debouncedSearch = useDebounce(search, 500)
</script>组合式函数规范
- 命名:使用
useXxx的驼峰命名 - 输入:参数尽量传 ref 而非原始值
- 返回值:返回包含多个 ref 的对象,支持解构
依赖注入
provide / inject 与组合式函数配合
js
// composables/useTheme.js
import { provide, inject, ref } from 'vue'
const ThemeKey = Symbol('theme')
export function useThemeProvider() {
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeKey, { theme, toggleTheme })
}
export function useTheme() {
const context = inject(ThemeKey)
if (!context) {
throw new Error('useTheme() 必须在 useThemeProvider() 之后使用')
}
return context
}// composables/useTheme.js
import { provide, inject, ref } from 'vue'
const ThemeKey = Symbol('theme')
export function useThemeProvider() {
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeKey, { theme, toggleTheme })
}
export function useTheme() {
const context = inject(ThemeKey)
if (!context) {
throw new Error('useTheme() 必须在 useThemeProvider() 之后使用')
}
return context
}vue
<!-- 祖先组件 -->
<script setup>
import { useThemeProvider } from '@/composables/useTheme'
useThemeProvider()
</script>
<!-- 任意后代组件 -->
<script setup>
import { useTheme } from '@/composables/useTheme'
const { theme, toggleTheme } = useTheme()
</script><!-- 祖先组件 -->
<script setup>
import { useThemeProvider } from '@/composables/useTheme'
useThemeProvider()
</script>
<!-- 任意后代组件 -->
<script setup>
import { useTheme } from '@/composables/useTheme'
const { theme, toggleTheme } = useTheme()
</script>类型推导
defineComponent + TypeScript
ts
import { defineComponent, ref, PropType } from 'vue'
interface User {
name: string
age: number
}
export default defineComponent({
props: {
title: String,
count: {
type: Number,
default: 0
},
user: {
type: Object as PropType<User>,
required: true
}
},
setup(props) {
// props.user 自动推导为 User 类型
const message = ref('Hello')
return { message }
}
})import { defineComponent, ref, PropType } from 'vue'
interface User {
name: string
age: number
}
export default defineComponent({
props: {
title: String,
count: {
type: Number,
default: 0
},
user: {
type: Object as PropType<User>,
required: true
}
},
setup(props) {
// props.user 自动推导为 User 类型
const message = ref('Hello')
return { message }
}
})script setup + TypeScript(推荐)
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
name: string
age: number
}
// 类型自动推导
const count = ref(0) // Ref<number>
const user = ref<User | null>(null)
const double = computed(() => count.value * 2) // ComputedRef<number>
function formatName(name: string): string {
return name.toUpperCase()
}
</script><script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
name: string
age: number
}
// 类型自动推导
const count = ref(0) // Ref<number>
const user = ref<User | null>(null)
const double = computed(() => count.value * 2) // ComputedRef<number>
function formatName(name: string): string {
return name.toUpperCase()
}
</script>
Cao Note