Skip to content

Vue3组合式API

2023-04-06
Author:lzugis

一、什么是组合式API

1. 基础概念

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api。在 Vue 3 中,组合式 API 基本上都会配合<script setup>语法在单文件组件中使用。

2. 为什么要有组合式API

  • 更好的逻辑复用: 在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷。
  • 更灵活的代码组织
  • 更好的类型推导
  • 更小的生产包体积

二、组件核心 API 的使用

1. 定义组件的 props

通过defineProps指定当前 props 类型,获得上下文的props对象。示例:

html
<script setup>
  import { defineProps } from 'vue'

  const props = defineProps({
    title: String,
    bar?: number
  })
</script>
<!-- 或者 -->
<script setup lang="ts"> 
    import { defineProps } from 'vue';
    
    type Props = { 
        msg: string,
        bar?: number
    }
    defineProps<Props>(); 
</script>

2. 定义 emit

使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行emit。示例:

html
<script setup>
  import { defineEmits } from 'vue'

  const emit = defineEmits(['change', 'delete'])
</script>

3. 父子组件通信

defineProps 用来接收父组件传来的 props ; defineEmits 用来声明触发的事件。

html
//父组件
<template>
  <my-son foo="🚀🚀🚀🚀🚀🚀" @childClick="childClick" />
</template>

<script lang="ts" setup>
import MySon from "./MySon.vue";

let childClick = (e: any):void => {
  console.log('from son:',e);  //🚀🚀🚀🚀🚀🚀
};
</script>


//子组件
<template>
  <span @click="sonToFather">信息:{{ props.foo }}</span>
</template>

<script lang="ts" setup>
import { defineEmits, defineProps} from "vue";

const emit = defineEmits(["childClick"]);     // 声明触发事件 childClick
const props = defineProps({ foo: String });   // 获取props

const sonToFather = () =>{
    emit('childClick' , props.foo)
}
</script>

4. 定义响应变量、函数、监听、计算属性computed

html
<template>
  <div>
    <span>{{ count }}</span>
  </div>
  <div>
    <span>{{ howCount }}</span>
  </div>
</template>
<script setup lang="ts"> 
import { ref,computed,watchEffect } from 'vue';

const count = ref(0); /

const addCount = () => { //定义函数,使用同上 
    count.value++; 
} 

//定义计算属性
const howCount = computed(()=>"现在count值为:"+count.value);

//定义监听
watchEffect(() => console.log(count.value)); 
</script>
  • watchEffect: 用于有副作用的操作,会自动收集依赖。无需区分deepimmediate,只要依赖的数据发生变化,就会调用。

5. reactive

此时name只会在初次创建的时候进行赋值,如果中间想要改变name的值,那么需要借助composition api 中的reactive。

html
<script setup lang="ts">
import { reactive, onUnmounted } from 'vue'

const state = reactive({
    counter: 0
})
// 定时器 每秒都会更新数据
const timer = setInterval(() => {
    state.counter++
}, 1000);

onUnmounted(() => {
    clearInterval(timer);
})
</script>
<template>
    <div>{{state.counter}}</div>
</template>

使用ref也能达到我们预期的counter,并且在模板中,vue进行了处理,我们可以直接使用counter而不用写counter.value.

6. ref暴露变量到模板

js
<script setup lang="ts">
import { ref } from 'vue'

const counter = ref(0);//不用 return ,直接在 templete 中使用

const timer = setInterval(() => {
    counter.value++
}, 1000)

onUnmounted(() => {
    clearInterval(timer);
})
</script>
<template>
    <div>{{counter}}</div>
</template>

7. 生命周期方法

因为setup是围绕beforeCreatecreated生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写。

可以通过在生命周期钩子前面加上on来访问组件的生命周期钩子。下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated
html
<script setup lang="ts"> 
import { onMounted } from 'vue';

onMounted(() => { console.log('mounted!'); });
</script>

8. 获取 slots 和 attrs

通过useAttrsuseSlots获取 attrs 数据和插槽。

html
<template>
    <component v-bind='attrs'></component>
</template>
<srcipt setup lang='ts'>
  import { useAttrs, useSlots } from 'vue'
  const attrs = useAttrs()
  const slots = useSlots()
<script>

9. defineExpose API

如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:

html
//父组件
<template>
  <Daughter ref="daughter" />
</template>

<script lang="ts" setup>
  import { ref } from "vue";
  import Daughter from "./Daughter.vue";

  const daughter = ref(null)
  console.log('🚀🚀🚀🚀~daughter', daughter)
</script>


//子组件
<template>
  <div>妾身{{ msg }}</div>
</template>

<script lang="ts" setup>
  import { ref, defineExpose} from "vue";
  const msg = ref('貂蝉')
  defineExpose({
      msg
  })
</script>

10. 自定义Hook

自定义Hook是组合式API中逻辑复用的核心方式,它允许我们将相关的响应式状态和方法封装在一起。

html
<!-- useCounter.js -->
<script setup>
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  const double = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    double
  }
}
</script>

<!-- 在组件中使用 -->
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

<script setup>
import { useCounter } from './useCounter'

const { count, increment, decrement, reset, double } = useCounter(10)
</script>

11. 响应式系统深入

html
<script setup>
import { ref, reactive, readonly, shallowRef, shallowReactive, toRef, toRefs, isRef, unref } from 'vue'

// ref vs reactive
const countRef = ref(0) // 基本类型用ref
const stateReactive = reactive({ // 对象用reactive
  name: 'Vue3',
  version: '3.0'
})

// readonly - 创建只读响应式对象
const readonlyState = readonly(stateReactive)

// shallowRef 和 shallowReactive - 浅层响应式
const shallowState = shallowReactive({
  deep: {
    nested: 'value'
  }
})

const shallowCount = shallowRef({ count: 0 })

// toRef - 创建响应式对象的属性ref
const nameRef = toRef(stateReactive, 'name')

// toRefs - 解构响应式对象
const { name, version } = toRefs(stateReactive)

// 类型检查
console.log(isRef(countRef)) // true
console.log(isRef(stateReactive)) // false

// unref - 获取ref的值或原始值
const value = unref(countRef)
</script>

12. 高级监听器

html
<script setup>
import { ref, watch, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'

const count = ref(0)
const name = ref('Vue')
const user = ref({ age: 25 })

// watch - 监听特定数据源
watch(count, (newVal, oldVal) => {
  console.log(`Count changed: ${oldVal} -> ${newVal}`)
})

// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('Multiple values changed')
})

// 深度监听
watch(user, (newVal, oldVal) => {
  console.log('User changed')
}, { deep: true })

// 立即执行
watch(count, (newVal) => {
  console.log(`Count is now: ${newVal}`)
}, { immediate: true })

// watchEffect - 自动收集依赖
watchEffect(() => {
  console.log(`Count is: ${count.value}`)
})

// watchPostEffect - 在DOM更新后执行
watchPostEffect(() => {
  console.log('DOM updated')
})

// watchSyncEffect - 同步执行
watchSyncEffect(() => {
  console.log('Sync effect')
})
</script>

13. 依赖注入

html
<!-- 父组件 -->
<template>
  <ChildComponent />
</template>

<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 提供响应式数据
const theme = ref('light')
const user = ref({ name: 'John', age: 30 })

provide('theme', readonly(theme)) // 提供只读的响应式数据
provide('user', user)

// 提供方法
provide('updateTheme', (newTheme) => {
  theme.value = newTheme
})
</script>

<!-- 子组件 -->
<template>
  <div :class="`theme-${theme}`">
    <p>{{ user.name }} is {{ user.age }} years old</p>
    <button @click="changeTheme">Change Theme</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'dark') // 带默认值
const user = inject('user')
const updateTheme = inject('updateTheme')

const changeTheme = () => {
  updateTheme(theme.value === 'light' ? 'dark' : 'light')
}
</script>

14. 模板引用

html
<template>
  <div ref="divRef">Hello</div>
  <input ref="inputRef" type="text" />
  <MyComponent ref="componentRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

const divRef = ref(null)
const inputRef = ref(null)
const componentRef = ref(null)

onMounted(() => {
  // 访问DOM元素
  console.log(divRef.value) // div元素
  console.log(inputRef.value) // input元素
  
  // 访问组件实例
  console.log(componentRef.value) // 组件实例
  
  // 聚焦输入框
  inputRef.value.focus()
  
  // 调用组件方法
  componentRef.value.someMethod()
})
</script>

15. Teleport - 传送门

html
<!-- 模态框组件 -->
<template>
  <teleport to="body">
    <div v-if="isOpen" class="modal-overlay">
      <div class="modal">
        <h3>{{ title }}</h3>
        <slot></slot>
        <button @click="close">关闭</button>
      </div>
    </div>
  </teleport>
</template>

<script setup>
import { ref, watch } from 'vue'

const props = defineProps({
  isOpen: Boolean,
  title: String
})

const emit = defineEmits(['close'])

const close = () => {
  emit('close')
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>

16. Suspense - 异步组件

html
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
</script>

<!-- 异步组件 -->
<template>
  <div>{{ message }}</div>
</template>

<script setup>
import { ref } from 'vue'

const message = ref('')
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 2000))
message.value = 'Loaded successfully!'
</script>

17. 组合式函数最佳实践

javascript
// composables/useFetch.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  let controller = null
  
  const fetchData = async () => {
    if (controller) {
      controller.abort()
    }
    
    controller = new AbortController()
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, {
        signal: controller.signal
      })
      data.value = await response.json()
    } catch (err) {
      if (err.name !== 'AbortError') {
        error.value = err
      }
    } finally {
      loading.value = false
    }
  }
  
  onMounted(fetchData)
  onUnmounted(() => {
    if (controller) {
      controller.abort()
    }
  })
  
  return {
    data,
    error,
    loading,
    refetch: fetchData
  }
}

18. 状态管理模式

javascript
// stores/useCounterStore.js
import { ref, computed, watch } from 'vue'

const globalState = ref({
  count: 0,
  name: 'Global Store'
})

export function useCounterStore() {
  const count = computed(() => globalState.value.count)
  const name = computed(() => globalState.value.name)
  
  const increment = () => {
    globalState.value.count++
  }
  
  const decrement = () => {
    globalState.value.count--
  }
  
  const reset = () => {
    globalState.value.count = 0
  }
  
  const setName = (newName) => {
    globalState.value.name = newName
  }
  
  // 持久化到localStorage
  watch(globalState, (newState) => {
    localStorage.setItem('counterStore', JSON.stringify(newState))
  }, { deep: true })
  
  return {
    count,
    name,
    increment,
    decrement,
    reset,
    setName
  }
}

19. TypeScript集成

html
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// 定义接口
interface User {
  id: number
  name: string
  age: number
}

interface Props {
  title: string
  user?: User
  count: number
}

// 定义Props类型
const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// 定义Emits类型
const emit = defineEmits<{
  update: [value: string]
  change: [id: number, name: string]
}>()

// 响应式数据类型
const users: Ref<User[]> = ref([])
const currentUser = ref<User | null>(null)

// 计算属性类型
const adultUsers = computed<User[]>(() => 
  users.value.filter(user => user.age >= 18)
)

// 函数类型
const updateUser = (id: number, updates: Partial<User>) => {
  const index = users.value.findIndex(user => user.id === id)
  if (index !== -1) {
    users.value[index] = { ...users.value[index], ...updates }
  }
}

// 使用
const handleClick = () => {
  emit('update', 'new value')
  emit('change', 1, 'John')
}
</script>

20. 性能优化技巧

html
<script setup>
import { ref, shallowRef, markRaw, computed } from 'vue'

// 使用shallowRef优化大对象
const largeData = shallowRef({ /* 大量数据 */ })

// 使用markRaw避免响应式转换
const nonReactiveObject = markRaw({
  // 不需要响应式的对象
})

// 计算属性缓存
const expensiveComputed = computed(() => {
  // 昂贵的计算
  return heavyCalculation()
})

// 避免不必要的响应式
const staticData = Object.freeze({
  // 静态配置
})

// 使用v-once优化静态内容
</script>

<template>
  <div v-once>
    <!-- 静态内容只渲染一次 -->
    <h1>{{ staticTitle }}</h1>
  </div>
  
  <!-- 动态内容 -->
  <div>{{ dynamicContent }}</div>
</template>

三、最佳实践与模式

1. 代码组织

javascript
// 按功能组织代码
// 1. 响应式数据
// 2. 计算属性  
// 3. 方法
// 4. 生命周期钩子
// 5. 监听器

<script setup>
import { ref, computed, onMounted, watch } from 'vue'

// 响应式数据
const state = reactive({
  users: [],
  loading: false,
  error: null
})

// 计算属性
const filteredUsers = computed(() => {
  return state.users.filter(user => user.active)
})

// 方法
const fetchUsers = async () => {
  state.loading = true
  try {
    const response = await fetch('/api/users')
    state.users = await response.json()
  } catch (error) {
    state.error = error
  } finally {
    state.loading = false
  }
}

// 生命周期钩子
onMounted(() => {
  fetchUsers()
})

// 监听器
watch(() => state.users, (newUsers) => {
  console.log('Users updated:', newUsers.length)
})
</script>

2. 错误处理

html
<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)

// 全局错误捕获
onErrorCaptured((err, instance, info) => {
  error.value = err.message
  console.error('Error captured:', err, info)
  return false // 阻止错误继续向上传播
})

// 异步错误处理
const asyncOperation = async () => {
  try {
    const result = await someAsyncFunction()
    return result
  } catch (error) {
    error.value = error.message
    throw error
  }
}
</script>

<template>
  <div v-if="error" class="error">
    错误: {{ error }}
  </div>
  <slot v-else />
</template>

3. 组件设计原则

javascript
// 单一职责原则
// 组件只做一件事,做好一件事

// 纯展示组件
const PresentationComponent = {
  props: ['data'],
  setup(props) {
    // 只负责展示,不修改数据
    return { formattedData: formatData(props.data) }
  }
}

// 容器组件
const ContainerComponent = {
  setup() {
    const data = ref([])
    const loading = ref(false)
    
    const fetchData = async () => {
      loading.value = true
      data.value = await api.getData()
      loading.value = false
    }
    
    return { data, loading, fetchData }
  }
}

四、调试技巧

1. Vue DevTools

javascript
// 在组件中添加调试信息
<script setup>
import { ref, onMounted } from 'vue'

const debug = process.env.NODE_ENV === 'development'

const state = ref({
  users: [],
  loading: false
})

if (debug) {
  // 开发环境下的调试代码
  window.debugState = state
  console.log('Debug mode enabled')
}
</script>

2. 性能分析

html
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  if (process.env.NODE_ENV === 'development') {
    // 性能标记
    performance.mark('component-mounted')
    
    // 性能测量
    setTimeout(() => {
      performance.mark('component-ready')
      performance.measure(
        'component-load-time',
        'component-mounted',
        'component-ready'
      )
      
      const measure = performance.getEntriesByName('component-load-time')[0]
      console.log(`Component load time: ${measure.duration}ms`)
    }, 0)
  }
})
</script>

总结

Vue3的组合式API提供了更灵活、更强大的组件开发方式:

  1. 更好的逻辑复用:通过自定义Hook实现代码复用
  2. 更清晰的代码组织:相关逻辑可以组织在一起
  3. 更好的类型推导:原生TypeScript支持
  4. 更小的包体积:tree-shaking友好
  5. 更强的性能:响应式系统优化

掌握组合式API的关键是理解响应式原理和合理组织代码结构。在实际项目中,建议结合业务需求选择合适的模式,保持代码的可维护性和可扩展性。


学习资源

官方文档

  1. Vue3官方文档
  2. 组合式API文档
  3. Reactivity API

学习教程

  1. Vue3组合式API实战教程
  2. Vue Mastery
  3. Vue School

实践项目

  1. Vue3 + TypeScript 最佳实践
  2. Pinia状态管理

社区资源

  1. Vue.js Discord
  2. Vue.js Twitter