038_Vue 3 核心知识体系第三部分:组合式 API 和高级特性

11. 组合式 API

11.1 为什么需要组合式 API

javascript

// 选项式 API 的问题
export default {
  data() { return { a: 1, b: 2 } },
  computed: { sum() { return this.a + this.b } },
  watch: { a() { /* 逻辑 */ } },
  methods: { method1() { /* 逻辑 */ } },
  // 相关逻辑分散在不同选项中
}

// 组合式 API 的优势:逻辑关注点聚焦
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const a = ref(1)
    const b = ref(2)
    
    const sum = computed(() => a.value + b.value)
    
    watch(a, () => { /* 逻辑 */ })
    
    const method1 = () => { /* 逻辑 */ }
    
    // 所有相关逻辑在一起
    return { a, b, sum, method1 }
  }
}

11.2 setup() 函数

vue

<script>
export default {
  // setup 函数
  setup(props, context) {
    // props: 响应式的 props 对象
    // context: 上下文对象,包含:
    //   - attrs: 非响应式对象,接收未在 props 中声明的属性
    //   - slots: 插槽对象
    //   - emit: 触发事件的函数
    //   - expose: 暴露公共属性的函数
    
    console.log(props.title)
    console.log(context.attrs)
    
    const count = ref(0)
    
    // 暴露给模板
    return {
      count
    }
    
    // 或者使用渲染函数
    // return () => h('div', count.value)
  }
}
</script>

11.3<script setup>语法糖

vue

<script setup>
// 1. 顶层绑定自动暴露给模板
const count = ref(0)
const increment = () => count.value++

// 2. 自动导入组件
import MyComponent from './MyComponent.vue'
// 可以直接在模板中使用 <MyComponent />

// 3. 使用 defineProps 和 defineEmits
const props = defineProps(['title'])
const emit = defineEmits(['update'])

// 4. 使用 defineExpose 暴露组件实例
defineExpose({
  count,
  increment
})

// 5. 使用 useSlots 和 useAttrs
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()

// 6. 顶层 await
const data = await fetchData()

// 7. 自定义指令
const vMyDirective = {
  mounted(el) {
    el.focus()
  }
}
</script>

11.4 组合式函数 (Composables)

javascript

// useMouse.js - 鼠标位置跟踪
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  const update = (event) => {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  
  return { x, y }
}

// useFetch.js - 数据获取
import { ref, watch } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  const fetchData = async () => {
    loading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  watch(() => url, fetchData, { immediate: true })
  
  return { data, error, loading, retry: fetchData }
}

// useLocalStorage.js - 本地存储
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const data = ref(defaultValue)
  
  // 从 localStorage 读取
  const storedValue = localStorage.getItem(key)
  if (storedValue !== null) {
    data.value = JSON.parse(storedValue)
  }
  
  // 监听变化并保存到 localStorage
  watch(data, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return data
}

// 在组件中使用
<script setup>
import { useMouse, useFetch, useLocalStorage } from './composables'

const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/data')
const settings = useLocalStorage('settings', { theme: 'dark' })
</script>

11.5 依赖注入模式

javascript

// useTheme.js - 主题管理
import { inject, provide, readonly, ref } from 'vue'

const ThemeSymbol = Symbol('theme')

export function provideTheme() {
  const theme = ref('light')
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  
  provide(ThemeSymbol, {
    theme: readonly(theme), // 只读,防止子组件直接修改
    toggleTheme
  })
}

export function useTheme() {
  const themeContext = inject(ThemeSymbol)
  
  if (!themeContext) {
    throw new Error('必须在 ThemeProvider 中使用')
  }
  
  return themeContext
}

// 在根组件中
<script setup>
import { provideTheme } from './useTheme'
provideTheme()
</script>

// 在子组件中
<script setup>
import { useTheme } from './useTheme'
const { theme, toggleTheme } = useTheme()
</script>

12. 渲染函数和 JSX

12.1 渲染函数基础

vue

<script>
import { h } from 'vue'

export default {
  render() {
    // h() 创建虚拟 DOM
    return h(
      'div', // 标签名
      { id: 'app', class: 'container' }, // 属性
      [
        h('h1', 'Hello World'),
        h('p', '这是内容'),
        this.$slots.default?.() // 插槽内容
      ]
    )
  }
}
</script>

12.2 渲染函数参数

javascript

export default {
  render() {
    // h 函数的三种形式:
    // 1. h('div') - 元素
    // 2. h(MyComponent) - 组件
    // 3. h('div', {}, children) - 带子节点
    
    return h(
      'button',
      {
        // 属性
        class: ['btn', { 'btn-primary': this.primary }],
        style: { color: this.color },
        // 事件
        onClick: this.handleClick,
        // 其他属性
        id: 'my-button',
        'data-test': 'button'
      },
      [
        // 子节点
        h('span', '按钮'),
        this.$slots.icon && h('span', this.$slots.icon())
      ]
    )
  }
}

12.3 使用 JSX

vue

<script>
// 配置 vite.config.js 支持 JSX
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const count = ref(0)
    
    const increment = () => {
      count.value++
    }
    
    // JSX 语法
    return () => (
      <div class="container">
        <h1>计数器: {count.value}</h1>
        <button onClick={increment}>增加</button>
        <ChildComponent title="子组件" />
      </div>
    )
  }
})

// 或者使用 .jsx/.tsx 文件
import { defineComponent } from 'vue'

const MyComponent = defineComponent({
  render() {
    return (
      <div>
        <slot name="header" />
        <main>{this.$slots.default?.()}</main>
        <slot name="footer" />
      </div>
    )
  }
})
</script>

12.4 函数式组件 + JSX

jsx

// FunctionalComponent.jsx
import { defineComponent } from 'vue'

const FunctionalComponent = (props, { slots, emit, attrs }) => {
  const handleClick = () => {
    emit('click', props.id)
  }
  
  return (
    <div {...attrs} onClick={handleClick}>
      {slots.default?.()}
      {props.title && <h2>{props.title}</h2>}
    </div>
  )
}

FunctionalComponent.props = ['id', 'title']
FunctionalComponent.emits = ['click']

export default FunctionalComponent

13. TypeScript 支持

13.1 组件类型定义

vue

<!-- 选项式 API + TypeScript -->
<script lang="ts">
import { defineComponent } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

export default defineComponent({
  name: 'UserProfile',
  
  props: {
    // Prop 类型
    userId: {
      type: Number as () => number,
      required: true
    },
    user: {
      type: Object as () => User,
      default: () => ({ id: 0, name: '', email: '' })
    },
    tags: {
      type: Array as () => string[],
      default: () => []
    }
  },
  
  data() {
    return {
      loading: false as boolean,
      error: null as string | null
    }
  },
  
  computed: {
    // 计算属性类型
    fullName(): string {
      return this.user.name.toUpperCase()
    }
  },
  
  methods: {
    // 方法类型
    updateUser(user: User): Promise<void> {
      return api.updateUser(user)
    }
  }
})
</script>

13.2 组合式 API + TypeScript

vue

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

// 接口定义
interface Props {
  title: string
  count?: number
  items: string[]
  user?: {
    name: string
    age: number
  }
}

interface Emits {
  (e: 'update:title', value: string): void
  (e: 'submit', data: { id: number; name: string }): void
}

// 类型化的 props
const props = defineProps<Props>()

// 类型化的 emits
const emit = defineEmits<Emits>()

// 响应式数据
const count = ref<number>(0) // 明确类型
const user = ref<User | null>(null) // 可为 null

// 计算属性
const total = computed<number>(() => {
  return props.count || 0 + count.value
})

// 异步函数
const fetchData = async (): Promise<void> => {
  const response = await fetch('/api/data')
  const data = await response.json()
  user.value = data
}
</script>

13.3 泛型组件

vue

<!-- GenericComponent.vue -->
<script setup lang="ts" generic="T extends { id: number; name: string }">
import { ref } from 'vue'

const props = defineProps<{
  items: T[]
  selected: T | null
}>()

const emit = defineEmits<{
  select: [item: T]
}>()

const selectItem = (item: T) => {
  emit('select', item)
}
</script>

<!-- 使用 -->
<template>
  <GenericComponent
    :items="users"
    :selected="selectedUser"
    @select="handleSelect"
  />
</template>

13.4 全局类型扩展

typescript

// src/types/vue.d.ts
import { ComponentCustomProperties } from 'vue'

declare module 'vue' {
  interface ComponentCustomProperties {
    $filters: {
      formatDate: (date: Date) => string
      formatCurrency: (amount: number) => string
    }
    $api: {
      fetchUser: (id: number) => Promise<User>
    }
  }
  
  // 全局组件类型
  interface GlobalComponents {
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
  }
}

14. 性能优化

14.1 响应式优化

javascript

// 1. 使用 shallowRef 和 shallowReactive
import { shallowRef, shallowReactive } from 'vue'

// 只追踪 .value 的变化
const largeObject = shallowRef({ /* 大对象 */ })

// 只追踪顶层属性的变化
const shallowState = shallowReactive({
  nested: { a: 1 } // nested 的变化不会触发响应
})

// 2. 使用 markRaw 跳过响应式转换
import { markRaw } from 'vue'

const nonReactiveConfig = markRaw({
  // 不会被转换为响应式
  constantValue: '不会变化'
})

// 3. 使用 computed 缓存计算结果
const expensiveValue = computed(() => {
  // 复杂计算
  return heavyCalculation()
})

// 4. 避免不必要的响应式
const nonReactiveArray = Object.freeze([1, 2, 3]) // 冻结数组
const nonReactiveObject = Object.freeze({ a: 1 }) // 冻结对象

14.2 组件优化

vue

<!-- 1. 使用 v-once -->
<template>
  <div v-once>
    <h1>{{ title }}</h1> <!-- 只渲染一次 -->
  </div>
</template>

<!-- 2. 使用 v-memo -->
<template>
  <div v-memo="[valueA, valueB]">
    <!-- 只有 valueA 或 valueB 变化时才更新 -->
    {{ valueA }} {{ valueB }}
  </div>
</template>

<!-- 3. 懒加载组件 -->
<script setup>
import { defineAsyncComponent } from 'vue'

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

<!-- 4. 函数式组件(轻量级) -->
<script>
export default {
  functional: true,
  props: ['item'],
  render(h, { props }) {
    return h('div', props.item.name)
  }
}
</script>

14.3 列表优化

vue

<template>
  <!-- 1. 使用 key -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- 2. 虚拟滚动 -->
  <!-- 使用 vue-virtual-scroller 等库 -->
  
  <!-- 3. 分页/懒加载 -->
  <div v-for="item in visibleItems">
    {{ item.name }}
  </div>
  <button @click="loadMore">加载更多</button>
</template>

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

const items = ref([])
const page = ref(1)
const pageSize = 20

// 只渲染当前页的数据
const visibleItems = computed(() => {
  const start = (page.value - 1) * pageSize
  return items.value.slice(start, start + pageSize)
})
</script>

14.4 内存优化

javascript

// 1. 及时清理副作用
import { onUnmounted } from 'vue'

onMounted(() => {
  const timer = setInterval(() => {
    // 定时任务
  }, 1000)
  
  // 组件销毁时清理
  onUnmounted(() => clearInterval(timer))
})

// 2. 避免内存泄漏
const listeners = []

onMounted(() => {
  listeners.push(
    window.addEventListener('resize', handleResize)
  )
})

onUnmounted(() => {
  listeners.forEach(removeListener => removeListener())
})

// 3. 使用 WeakMap/WeakSet 存储临时数据
const cache = new WeakMap()

function getExpensiveValue(obj) {
  if (!cache.has(obj)) {
    const value = computeExpensiveValue(obj)
    cache.set(obj, value)
  }
  return cache.get(obj)
}

15. 最佳实践

15.1 项目结构

text

src/
├── assets/           # 静态资源
├── components/       # 公共组件
│   ├── common/      # 通用组件
│   ├── layout/      # 布局组件
│   └── ui/          # UI 组件
├── composables/      # 组合式函数
├── views/           # 页面组件
├── router/          # 路由配置
├── store/           # 状态管理
├── api/             # API 接口
├── utils/           # 工具函数
├── types/           # TypeScript 类型
└── styles/          # 样式文件

15.2 代码规范

javascript

// 1. 命名规范
const userData = ref(null)          // ✅ 描述性名称
const data = ref(null)              // ❌ 过于模糊
const getUserInformation = () => {} // ✅ 动词开头
const userInfo = () => {}           // ❌ 名词开头

// 2. 组件命名
MyComponent.vue      // ✅ PascalCase
my-component.vue     // ✅ kebab-case(模板中使用)
MyComponent.ts       // ✅ TypeScript 组件

// 3. 文件组织
// 单文件组件顺序
<template>
  <!-- 1. 模板 -->
</template>

<script setup>
  // 2. 脚本
</script>

<style scoped>
  /* 3. 样式 */
</style>

// 4. 导入顺序
// 1) Vue 核心 API
// 2) 第三方库
// 3) 本地模块
// 4) 类型导入

15.3 错误处理

javascript

// 1. 全局错误处理
import { createApp } from 'vue'

const app = createApp(App)

app.config.errorHandler = (err, instance, info) => {
  console.error('Vue 错误:', err)
  // 发送错误日志
}

// 2. Promise 错误处理
const fetchData = async () => {
  try {
    const data = await api.getData()
    return data
  } catch (error) {
    console.error('请求失败:', error)
    // 显示用户友善的错误信息
    showError('加载失败,请重试')
    throw error // 重新抛出供上层处理
  }
}

// 3. 错误边界组件
<template>
  <slot v-if="hasError" name="fallback" />
  <slot v-else />
</template>

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

const hasError = ref(false)

onErrorCaptured((error, instance, info) => {
  hasError.value = true
  console.error('子组件错误:', error)
  // 返回 false 阻止错误继续向上传播
  return false
})
</script>

15.4 测试

javascript

// 1. 单元测试示例(使用 Vitest)
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('点击按钮增加计数', async () => {
    const wrapper = mount(Counter)
    
    // 初始状态
    expect(wrapper.find('.count').text()).toBe('0')
    
    // 触发事件
    await wrapper.find('button').trigger('click')
    
    // 断言结果
    expect(wrapper.find('.count').text()).toBe('1')
  })
})

// 2. 组合式函数测试
import { useCounter } from './useCounter'
import { renderHook, act } from '@testing-library/vue'

describe('useCounter', () => {
  it('应该增加计数', () => {
    const { result } = renderHook(() => useCounter())
    
    expect(result.current.count.value).toBe(0)
    
    act(() => {
      result.current.increment()
    })
    
    expect(result.current.count.value).toBe(1)
  })
})

这是 Vue 3 核心知识的第三部分,涵盖了组合式 API、渲染函数、TypeScript 支持、性能优化和最佳实践。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容