Skip to content

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 中不能访问 this
  • setup 中可以使用 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 ​

特性refreactive
数据类型任意类型(基本类型+对象)对象/数组
访问方式.value(模板中自动解包)直接访问
重新赋值支持 count = ref(1)不支持直接替换整个对象
适用场景基本类型、需要重新赋值的变量复杂对象、表单数据
js
const arr = reactive([1, 2, 3])
// 支持索引修改
arr[0] = 10
// 支持新增
arr[3] = 4
const 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))  // true
import { ref, unref, isRef } from 'vue'

const count = ref(0)

console.log(unref(count))  // 0(相当于 isRef(val) ? val.value : val)
console.log(isRef(count))  // true

customRef ​

创建自定义的 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 ​

特性watchwatchEffect
依赖追踪显式指定监听源自动追踪
旧值可以访问旧值不能
懒执行默认懒执行(除非 immediate: true)立即执行
适用场景需要旧值、特定数据变化时不需要旧值、自动追踪依赖

生命周期钩子 ​

在 setup 中通过 onX 形式使用生命周期钩子。

选项式 API组合式 API说明
beforeCreate—setup 本身就是
created—setup 本身就是
beforeMountonBeforeMount挂载前
mountedonMounted已挂载
beforeUpdateonBeforeUpdate更新前
updatedonUpdated已更新
beforeUnmountonBeforeUnmount卸载前
unmountedonUnmounted已卸载
errorCapturedonErrorCaptured错误捕获
renderTrackedonRenderTracked调试:追踪响应式依赖
renderTriggeredonRenderTriggered调试:触发重新渲染
activatedonActivatedkeep-alive 激活
deactivatedonDeactivatedkeep-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>