Vue3组合式API:代码组织的艺术

Vue3组合式API:代码组织的艺术

Composition API

如果你写过大型Vue2项目,一定遇到过这样的困扰:

  • 一个组件几千行,data、methods、computed分散各处
  • 想复用一段逻辑,只能用mixin,然后命名冲突
  • TypeScript支持不完善,类型推断总有问题

组合式API(Composition API)就是为了解决这些问题而生的。它让你按功能组织代码,而不是按选项组织。

选项式API vs 组合式API

选项式API(Options API)


export default {
  data() {
    return {
      count: 0,
      user: null
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    fetchUser() {
      // ...
    }
  },
  mounted() {
    this.fetchUser()
  }
}

问题:

  • 相关代码分散在不同选项中
  • 逻辑复用困难(mixin有命名冲突)
  • TypeScript类型推断不好

组合式API(Composition API)


import { ref, computed, onMounted } from 'vue'

// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
  count.value++
}

// 用户逻辑
const user = ref(null)
async function fetchUser() {
  user.value = await fetch('/api/user').then(r => r.json())
}
onMounted(fetchUser)

优势:

  • 相关代码放在一起
  • 逻辑可以提取成独立函数
  • 完美的TypeScript支持

setup函数

``是setup函数的语法糖:

<!-- 等价于 -->

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}

<!-- 语法糖 -->

const count = ref(0)

所有顶层绑定自动暴露给模板。

Props和Emits


// 定义props
const props = defineProps({
  title: String
})

// 定义emits
const emit = defineEmits(['update'])

// 使用
console.log(props.title)
emit('update', data)

响应式转换

Vue3提供响应式语法糖(实验性):


// 启用后,ref可以省略.value
let count = $ref(0)
count++  // 自动处理

// 解构保持响应性
const { x, y } = $(useMouse())

组合式函数

组合式函数(Composables)是Vue3复用逻辑的核心方式。

基础示例

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}
<!-- 使用 -->

import { useCounter } from './useCounter'

const { count, doubleCount, increment } = useCounter(10)

  <p>Count: {{ count }}</p>
  <p>Double: {{ doubleCount }}</p>
  <button>+1</button>

实用组合式函数

useMouse - 追踪鼠标位置:

import { ref, onMounted, onUnmounted } 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))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

useFetch - 数据获取:

import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function fetch() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 支持响应式URL
  watchEffect(fetch)

  return { data, error, loading, refetch: fetch }
}

import { useFetch } from './useFetch'

const { data, loading, error } = useFetch('/api/users')

  <div>Loading...</div>
  <div>Error: {{ error.message }}</div>
  <div>
    <ul>
      <li>
        {{ user.name }}
      </li>
    </ul>
  </div>

useLocalStorage - 本地存储:

import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    (newVal) => {
      if (newVal === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newVal))
      }
    },
    { deep: true }
  )

  return data
}

import { useLocalStorage } from './useLocalStorage'

const theme = useLocalStorage('theme', 'light')

  <button>
    Toggle Theme
  </button>

provide/inject

跨层级组件通信:

<!-- 祖先组件 -->

import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
})
<!-- 后代组件(任意层级) -->

import { inject } from 'vue'

const theme = inject('theme')
const toggleTheme = inject('toggleTheme')

  <div>
    <button>Toggle</button>
  </div>

响应式provide

// 使用Symbol避免命名冲突
export const ThemeKey = Symbol('theme')

// 提供默认值
const theme = inject(ThemeKey, 'light')

// 只读
provide(ThemeKey, readonly(theme))

模板引用


import { ref, onMounted } from 'vue'

const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})

v-for中的ref


const listRef = ref([])

function setRef(el) {
  if (el) {
    listRef.value.push(el)
  }
}

  <div>
    {{ item }}
  </div>

组件ref


import ChildComponent from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  childRef.value.someMethod()
}

异步组件


import { defineAsyncComponent } from 'vue'

// 异步加载
const AsyncComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

最佳实践

命名约定

// 组合式函数以use开头
useCounter()
useMouse()
useFetch()

// 私有函数以_开头或使用__
function _internalHelper() {}

返回值

// 返回响应式引用
export function useCounter() {
  const count = ref(0)
  return { count }  // 不要返回 count.value
}

// 使用时保持响应性
const { count } = useCounter()
// count是ref,仍然是响应式的

清理副作用

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))

  // 或者返回清理函数
  return () => target.removeEventListener(event, callback)
}

// 使用
const cleanup = useEventListener(window, 'resize', handleResize)
cleanup()  // 手动清理

参数处理

// 支持ref和原始值
import { toValue } from 'vue'

export function useFetch(url) {
  // toValue会解包ref
  const finalUrl = toValue(url)

  watchEffect(() => {
    fetch(toValue(url))  // 响应式URL
  })
}

// 使用
const url = ref('/api/users')
const { data } = useFetch(url)  // url变化时自动重新fetch

小结

你现在掌握了:

  • 组合式API的核心思想(按功能组织代码)
  • 组合式函数的编写和使用
  • provide/inject跨层级通信
  • 模板引用和异步组件
  • 最佳实践和命名约定

下一期,我们进入Vue3生态,学习Vue Router路由和Pinia状态管理,完成一个实战项目。


练习任务

  1. 编写一个useCounter组合式函数
  2. 编写一个useDebounce防抖函数
  3. 使用provide/inject实现主题切换
  4. 实现一个异步加载的组件

下期预告:《Vue3生态:Router、Pinia与实战项目》—— 路由怎么配?状态怎么管?来做一个完整项目!

Views: 1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Index