Skip to content
刘恩义的技术博客
返回

Vue 3 Composition API 深度解析

Edit page

Vue 3 的 Composition API 是其最重要的特性之一,它彻底改变了 Vue 组件的组织方式和逻辑复用机制。本文将深入解析 Composition API 的设计原理、核心特性与高级用法,帮助你掌握现代 Vue 开发的精髓。

1. Composition API 设计原理

1.1 为什么需要 Composition API?

在 Vue 2 中,Options API 是组件组织的主要方式,但它存在一些局限性:

Composition API 的出现正是为了解决这些问题:

1.2 Composition API 的核心概念

Composition API 的核心概念包括:

1.3 Composition API vs Options API

特性Composition APIOptions API
逻辑组织按功能组织按选项组织
逻辑复用组合函数Mixins、HOC、Renderless
TypeScript 支持优秀有限
代码可读性高(相关逻辑集中)低(逻辑分散)
学习曲线中等

2. 响应式系统深度解析

2.1 响应式系统的工作原理

Vue 3 的响应式系统使用 Proxy 替代了 Vue 2 的 Object.defineProperty,带来了以下优势:

// Vue 3 响应式系统的核心实现原理
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      // 触发更新
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return result;
    },
    // 其他拦截器...
  });
}

2.2 ref vs reactive

refreactive 是创建响应式状态的两种主要方式:

import { ref, reactive, toRefs, isRef, unref } from 'vue';

// 使用 ref
const count = ref(0);
console.log(count.value); // 0
count.value = 1; // 触发更新

// 使用 reactive
const state = reactive({ count: 0 });
console.log(state.count); // 0
state.count = 1; // 触发更新

// 将 reactive 对象转换为 ref
const refs = toRefs(state);
console.log(refs.count.value); // 1

// 检查是否是 ref
console.log(isRef(count)); // true

// 自动解包 ref
const value = unref(count); // 等同于 count.value

2.3 响应式系统的高级用法

import { 
  shallowRef, 
  shallowReactive, 
  readonly, 
  markRaw, 
  customRef 
} from 'vue';

// 浅层响应式 ref
const shallow = shallowRef({ nested: { count: 0 } });
shallow.value.nested.count = 1; // 不会触发更新
shallow.value = { nested: { count: 2 } }; // 会触发更新

// 浅层响应式对象
const shallowObj = shallowReactive({ nested: { count: 0 } });
shallowObj.nested.count = 1; // 不会触发更新
shallowObj.nested = { count: 2 }; // 会触发更新

// 只读响应式对象
const readOnly = readonly({ count: 0 });
readOnly.count = 1; // 警告:无法修改只读对象

// 非响应式对象
const raw = markRaw({ count: 0 });
const wrapped = reactive(raw);
console.log(wrapped === raw); // true(返回原始对象)

// 自定义 ref
function useDebouncedRef(value, delay = 300) {
  let timeout;
  return customRef((track, trigger) => ({
    get() {
      track();
      return value;
    },
    set(newValue) {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        value = newValue;
        trigger();
      }, delay);
    }
  }));
}

const debouncedValue = useDebouncedRef('');

3. Composition API 高级用法

3.1 setup() 函数

setup() 函数是 Composition API 的入口点,在组件创建前执行:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  setup() {
    // 创建响应式状态
    const count = ref(0);
    
    // 定义方法
    const increment = () => {
      count.value++;
    };
    
    // 返回给模板使用的状态和方法
    return {
      count,
      increment
    };
  }
};
</script>

3.2 <script setup> 语法糖

<script setup> 是 Vue 3.2+ 引入的语法糖,它简化了 Composition API 的使用:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

// 直接声明的状态和方法会自动暴露给模板
const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

3.3 组合函数

组合函数是 Composition API 的核心,它允许我们将逻辑封装和复用:

// composables/useCounter.ts
import { ref, computed, onMounted } from 'vue';

export function useCounter(initialValue = 0, step = 1) {
  const count = ref(initialValue);
  const doubleCount = computed(() => count.value * 2);
  
  const increment = () => {
    count.value += step;
  };
  
  const decrement = () => {
    count.value -= step;
  };
  
  const reset = () => {
    count.value = initialValue;
  };
  
  // 生命周期钩子
  onMounted(() => {
    console.log('Counter mounted');
  });
  
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  };
}

// 在组件中使用
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="reset">Reset</button>
  </div>
</template>

<script setup>
import { useCounter } from '@/composables/useCounter';

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

3.4 依赖注入

使用 provide()inject() 实现组件间通信:

<!-- 父组件 -->
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent />
  </div>
</template>

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

// 提供状态
const theme = ref('light');
const updateTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
};

provide('theme', theme);
provide('updateTheme', updateTheme);
</script>

<!-- 子组件 -->
<template>
  <div :class="theme">
    <h2>Child Component</h2>
    <p>Theme: {{ theme }}</p>
    <button @click="updateTheme">Toggle Theme</button>
  </div>
</template>

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

// 注入状态
const theme = inject('theme');
const updateTheme = inject('updateTheme');
</script>

<style scoped>
.light {
  background-color: white;
  color: black;
}

.dark {
  background-color: black;
  color: white;
}
</style>

4. 生命周期钩子

4.1 生命周期钩子对照表

Vue 2 钩子Vue 3 组合式 API
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

4.2 生命周期钩子的使用

<template>
  <div>
    <h1>LifeCycle Demo</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';

const count = ref(0);
const increment = () => count.value++;

console.log('setup()');

onBeforeMount(() => {
  console.log('onBeforeMount()');
});

onMounted(() => {
  console.log('onMounted()');
});

onBeforeUpdate(() => {
  console.log('onBeforeUpdate()');
});

onUpdated(() => {
  console.log('onUpdated()');
});

onBeforeUnmount(() => {
  console.log('onBeforeUnmount()');
});

onUnmounted(() => {
  console.log('onUnmounted()');
});
</script>

5. 高级组合函数

5.1 异步组合函数

// composables/useAsyncData.ts
import { ref, computed, onMounted } from 'vue';

export function useAsyncData<T>(fetchFn: () => Promise<T>) {
  const data = ref<T | null>(null);
  const loading = ref(true);
  const error = ref<Error | null>(null);
  
  const execute = async () => {
    loading.value = true;
    error.value = null;
    
    try {
      data.value = await fetchFn();
    } catch (err) {
      error.value = err as Error;
    } finally {
      loading.value = false;
    }
  };
  
  onMounted(execute);
  
  return {
    data,
    loading,
    error,
    execute
  };
}

// 在组件中使用
<template>
  <div>
    <h1>Async Data Demo</h1>
    
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="data">
      <h2>{{ data.title }}</h2>
      <p>{{ data.body }}</p>
    </div>
    
    <button @click="execute">Refresh</button>
  </div>
</template>

<script setup>
import { useAsyncData } from '@/composables/useAsyncData';

interface Post {
  id: number;
  title: string;
  body: string;
}

const { data, loading, error, execute } = useAsyncData<Post>(async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  if (!response.ok) {
    throw new Error('Failed to fetch post');
  }
  return response.json();
});
</script>

5.2 表单处理组合函数

// composables/useForm.ts
import { ref, computed } from 'vue';

export function useForm<T extends Record<string, any>>(initialValues: T, validate?: (values: T) => Record<string, string>) {
  const values = ref<T>({ ...initialValues });
  const errors = ref<Record<string, string>>({});
  const touched = ref<Record<string, boolean>>({});
  
  const isValid = computed(() => {
    return Object.keys(errors.value).length === 0;
  });
  
  const handleChange = (e: Event) => {
    const target = e.target as HTMLInputElement;
    const { name, value, type, checked } = target;
    
    values.value[name as keyof T] = type === 'checkbox' ? checked : value;
    touched.value[name] = true;
    
    if (validate) {
      errors.value = validate(values.value);
    }
  };
  
  const handleSubmit = (onSubmit: (values: T) => void) => {
    // 标记所有字段为 touched
    Object.keys(values.value).forEach(key => {
      touched.value[key] = true;
    });
    
    // 验证
    if (validate) {
      errors.value = validate(values.value);
    }
    
    // 提交
    if (isValid.value) {
      onSubmit(values.value);
    }
  };
  
  const reset = () => {
    values.value = { ...initialValues };
    errors.value = {};
    touched.value = {};
  };
  
  return {
    values,
    errors,
    touched,
    isValid,
    handleChange,
    handleSubmit,
    reset
  };
}

// 在组件中使用
<template>
  <form @submit.prevent="handleSubmit(onSubmit)">
    <div>
      <label for="name">Name:</label>
      <input 
        id="name" 
        name="name" 
        type="text" 
        :value="values.name"
        @change="handleChange"
      />
      <p v-if="touched.name && errors.name" class="error">
        {{ errors.name }}
      </p>
    </div>
    
    <div>
      <label for="email">Email:</label>
      <input 
        id="email" 
        name="email" 
        type="email" 
        :value="values.email"
        @change="handleChange"
      />
      <p v-if="touched.email && errors.email" class="error">
        {{ errors.email }}
      </p>
    </div>
    
    <button type="submit" :disabled="!isValid">Submit</button>
    <button type="button" @click="reset">Reset</button>
  </form>
</template>

<script setup>
import { useForm } from '@/composables/useForm';

interface FormValues {
  name: string;
  email: string;
}

const { values, errors, touched, isValid, handleChange, handleSubmit, reset } = useForm<FormValues>(
  { name: '', email: '' },
  (values) => {
    const errors: Record<string, string> = {};
    
    if (!values.name) {
      errors.name = 'Name is required';
    }
    
    if (!values.email) {
      errors.email = 'Email is required';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
      errors.email = 'Email is invalid';
    }
    
    return errors;
  }
);

const onSubmit = (values: FormValues) => {
  console.log('Form submitted:', values);
  alert('Form submitted successfully!');
};
</script>

<style scoped>
.error {
  color: red;
  font-size: 12px;
}
</style>

5.3 键盘事件组合函数

// composables/useKeyPress.ts
import { onMounted, onUnmounted } from 'vue';

export function useKeyPress(targetKey: string, callback: () => void) {
  const handleKeyPress = (event: KeyboardEvent) => {
    if (event.key === targetKey) {
      callback();
    }
  };
  
  onMounted(() => {
    window.addEventListener('keydown', handleKeyPress);
  });
  
  onUnmounted(() => {
    window.removeEventListener('keydown', handleKeyPress);
  });
}

// 在组件中使用
<template>
  <div>
    <h1>Key Press Demo</h1>
    <p>Press the Escape key to toggle the modal</p>
    
    <div v-if="isModalOpen" class="modal">
      <div class="modal-content">
        <h2>Modal</h2>
        <p>This is a modal. Press Escape to close it.</p>
        <button @click="closeModal">Close</button>
      </div>
    </div>
    
    <button @click="openModal">Open Modal</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useKeyPress } from '@/composables/useKeyPress';

const isModalOpen = ref(false);

const openModal = () => {
  isModalOpen.value = true;
};

const closeModal = () => {
  isModalOpen.value = false;
};

// 使用组合函数监听 Escape 键
useKeyPress('Escape', () => {
  if (isModalOpen.value) {
    closeModal();
  }
});
</script>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

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

6. 性能优化

6.1 响应式优化

6.2 渲染优化

6.3 组合函数优化

<template>
  <div>
    <h1>Performance Optimization</h1>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
  </div>
</template>

<script setup>
import { ref, computed, watchEffect, onUnmounted } from 'vue';

const count = ref(0);

// 使用 computed 缓存计算结果
const doubleCount = computed(() => {
  console.log('Computing double count');
  return count.value * 2;
});

// 使用 watchEffect 监听变化
watchEffect(() => {
  console.log('Count changed:', count.value);
  
  // 清理函数
  return () => {
    console.log('Cleaning up watcher');
  };
});

// 模拟定时器
const timer = setInterval(() => {
  count.value++;
}, 1000);

// 清理定时器
onUnmounted(() => {
  clearInterval(timer);
});
</script>

7. 实战案例:电商购物车

7.1 功能需求

7.2 实现

<template>
  <div class="shopping-cart">
    <h1>Shopping Cart</h1>
    
    <!-- 商品列表 -->
    <div class="products">
      <h2>Products</h2>
      <div class="product-list">
        <div v-for="product in products" :key="product.id" class="product">
          <h3>{{ product.name }}</h3>
          <p>Price: ¥{{ product.price }}</p>
          <button @click="addProduct(product)">Add to Cart</button>
        </div>
      </div>
    </div>
    
    <!-- 购物车 -->
    <div class="cart">
      <h2>Cart</h2>
      
      <div v-if="cartItems.length === 0" class="empty-cart">
        Your cart is empty
      </div>
      
      <div v-else class="cart-items">
        <div v-for="item in cartItems" :key="item.id" class="cart-item">
          <h3>{{ item.name }}</h3>
          <p>Price: ¥{{ item.price }}</p>
          <div class="quantity-control">
            <button @click="decreaseQuantity(item.id)">-</button>
            <span>{{ item.quantity }}</span>
            <button @click="increaseQuantity(item.id)">+</button>
          </div>
          <button @click="removeItem(item.id)" class="remove-btn">Remove</button>
        </div>
        
        <div class="cart-summary">
          <h3>Total: ¥{{ totalPrice }}</h3>
          <button class="checkout-btn">Checkout</button>
        </div>
      </div>
    </div>
  </div>
</template>

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

interface Product {
  id: number;
  name: string;
  price: number;
}

interface CartItem extends Product {
  quantity: number;
}

// 模拟商品数据
const products = ref<Product[]>([
  { id: 1, name: 'iPhone 15', price: 6999 },
  { id: 2, name: 'MacBook Pro', price: 14999 },
  { id: 3, name: 'AirPods Pro', price: 1999 }
]);

// 购物车数据
const cartItems = ref<CartItem[]>([]);

// 计算总价
const totalPrice = computed(() => {
  return cartItems.value.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
});

// 添加商品到购物车
const addProduct = (product: Product) => {
  const existingItem = cartItems.value.find(item => item.id === product.id);
  
  if (existingItem) {
    existingItem.quantity++;
  } else {
    cartItems.value.push({
      ...product,
      quantity: 1
    });
  }
  
  saveCart();
};

// 从购物车移除商品
const removeItem = (id: number) => {
  cartItems.value = cartItems.value.filter(item => item.id !== id);
  saveCart();
};

// 增加商品数量
const increaseQuantity = (id: number) => {
  const item = cartItems.value.find(item => item.id === id);
  if (item) {
    item.quantity++;
    saveCart();
  }
};

// 减少商品数量
const decreaseQuantity = (id: number) => {
  const item = cartItems.value.find(item => item.id === id);
  if (item) {
    if (item.quantity > 1) {
      item.quantity--;
    } else {
      removeItem(id);
    }
    saveCart();
  }
};

// 保存购物车到本地存储
const saveCart = () => {
  localStorage.setItem('cart', JSON.stringify(cartItems.value));
};

// 从本地存储加载购物车
const loadCart = () => {
  const savedCart = localStorage.getItem('cart');
  if (savedCart) {
    cartItems.value = JSON.parse(savedCart);
  }
};

// 生命周期钩子
onMounted(() => {
  loadCart();
});
</script>

<style scoped>
.shopping-cart {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.products, .cart {
  margin: 40px 0;
}

.product-list, .cart-items {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

.product, .cart-item {
  border: 1px solid #ccc;
  padding: 20px;
  border-radius: 4px;
  width: 250px;
}

.quantity-control {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0;
}

.remove-btn {
  margin-top: 10px;
  background-color: #ff4757;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}

.cart-summary {
  margin-top: 20px;
  padding: 20px;
  border-top: 1px solid #ccc;
}

.checkout-btn {
  background-color: #2ed573;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.empty-cart {
  padding: 40px;
  text-align: center;
  background-color: #f5f5f5;
  border-radius: 4px;
}
</style>

8. 最佳实践总结

8.1 代码组织

8.2 响应式状态管理

8.3 组件设计

8.4 性能优化

8.5 测试

9. 未来展望

9.1 Vue 3 的发展

Vue 3 自发布以来,已经成为 Vue 生态系统的主流版本,它的 Composition API 也被广泛采用。未来,Vue 团队将继续改进 Composition API,使其更加完善和易用。

9.2 生态系统

Vue 3 的生态系统正在不断完善:

9.3 学习资源

总结

Vue 3 的 Composition API 是一项重大的技术革新,它为 Vue 开发者提供了一种更灵活、更强大的组件组织方式。通过本文的学习,你应该已经掌握了 Composition API 的核心概念、设计原理和高级用法。

Composition API 的优势在于:

随着 Vue 3 和 Composition API 的不断发展,它们将成为前端开发的重要工具。通过不断学习和实践,你可以构建出更加高质量、可维护的 Vue 应用。


Edit page

Previous Post
Spring Boot 3.3 新特性详解
Next Post
Vue 3 最佳实践