Vue组合式APISkill vue-composition-api

Vue组合式API是Vue 3框架的核心功能,用于通过组合式函数构建响应式、可维护和可复用的前端应用程序。它提供ref、reactive、computed、watch等核心特性,支持更好的代码组织、逻辑复用和TypeScript集成,适用于现代Vue开发,提高开发效率和代码质量。关键词:Vue 3, Composition API, 前端开发, 响应式编程, TypeScript, 组件化开发, 可复用逻辑, 生命周期钩子, 状态管理。

前端开发 0 次安装 2 次浏览 更新于 3/25/2026

名称: vue-composition-api 用户可调用: false 描述: 在Vue 3 Composition API中使用响应式ref、computed和composables时使用。用于构建现代Vue 3应用程序。 允许的工具:

  • Bash
  • Read

Vue Composition API

掌握Vue 3 Composition API,用于构建可扩展、可维护的Vue应用程序,具有更好的代码组织和可复用性。

Setup函数基础

setup()函数是使用Composition API的入口点:

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

export default {
  props: ['initialCount'],
  setup(props, context) {
    // props是响应式的
    console.log(props.initialCount);

    // context提供attrs、slots、emit、expose
    const { attrs, slots, emit, expose } = context;

    const count = ref(0);
    const doubled = computed(() => count.value * 2);

    function increment() {
      count.value++;
      emit('update', count.value);
    }

    onMounted(() => {
      console.log('组件已挂载');
    });

    // 暴露公共方法
    expose({ increment });

    // 返回值到模板
    return {
      count,
      doubled,
      increment
    };
  }
};

Script Setup语法

现代Vue 3使用<script setup>以获得更简洁的语法:

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

// 顶层绑定自动暴露给模板
const count = ref(0);
const doubled = computed(() => count.value * 2);

function increment() {
  count.value++;
}

// 使用编译器宏定义props和emits
interface Props {
  initialCount?: number;
}

const props = withDefaults(defineProps<Props>(), {
  initialCount: 0
});

const emit = defineEmits<{
  update: [value: number];
}>();
</script>

<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubled }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

Ref vs Reactive - 何时使用每种

使用Ref对于

import { ref } from 'vue';

// 原始值
const count = ref(0);
const name = ref('John');
const isActive = ref(true);

// 需要替换的单个对象
const user = ref({ name: 'John', age: 30 });
user.value = { name: 'Jane', age: 25 }; // 有效

// 需要替换的数组
const items = ref([1, 2, 3]);
items.value = [4, 5, 6]; // 有效

使用Reactive对于

import { reactive, toRefs } from 'vue';

// 复杂嵌套对象
const state = reactive({
  user: { name: 'John', age: 30 },
  settings: { theme: 'dark', notifications: true },
  posts: []
});

// 分组相关状态
const formState = reactive({
  name: '',
  email: '',
  password: '',
  errors: {}
});

// 转换为refs以进行解构
const { name, email } = toRefs(formState);

避免使用Reactive对于

// 不要:替换整个reactive对象会失去响应性
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 破坏响应性!

// 做:使用ref代替
const state = ref({ count: 0 });
state.value = { count: 1 }; // 有效

Computed属性模式

基础Computed

import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

可写Computed

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(value) {
    const names = value.split(' ');
    firstName.value = names[0] || '';
    lastName.value = names[1] || '';
  }
});

// 现在可以设置
fullName.value = 'Jane Smith';

复杂逻辑的Computed

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

const cart = ref<Product[]>([]);

const cartSummary = computed(() => {
  const total = cart.value.reduce((sum, item) =>
    sum + (item.price * item.quantity), 0
  );

  const itemCount = cart.value.reduce((sum, item) =>
    sum + item.quantity, 0
  );

  const tax = total * 0.08;
  const grandTotal = total + tax;

  return {
    total,
    itemCount,
    tax,
    grandTotal
  };
});

Watch和WatchEffect

Watch - 显式依赖

import { ref, watch } from 'vue';

const count = ref(0);
const name = ref('');

// 监视单个源
watch(count, (newValue, oldValue) => {
  console.log(`计数从 ${oldValue} 更改为 ${newValue}`);
});

// 监视多个源
watch(
  [count, name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log('多个值已更改');
  }
);

// 监视reactive对象属性
const user = reactive({ name: 'John', age: 30 });

watch(
  () => user.name,
  (newName) => {
    console.log(`名称更改为 ${newName}`);
  }
);

// 深度监视
watch(
  user,
  (newUser) => {
    console.log('用户已更改:', newUser);
  },
  { deep: true }
);

WatchEffect - 自动跟踪

import { ref, watchEffect } from 'vue';

const count = ref(0);
const multiplier = ref(2);

// 自动跟踪依赖
watchEffect(() => {
  console.log(`结果: ${count.value * multiplier.value}`);
});

// 立即运行并在依赖更改时运行

高级Watch选项

const data = ref(null);

watch(
  source,
  (newValue, oldValue) => {
    // 回调逻辑
  },
  {
    immediate: true,      // 立即运行
    deep: true,           // 深度监视对象
    flush: 'post',        // 时机: 'pre' | 'post' | 'sync'
    onTrack(e) {          // 调试
      console.log('跟踪', e);
    },
    onTrigger(e) {        // 调试
      console.log('触发', e);
    }
  }
);

// 停止监视
const stop = watch(source, callback);
stop(); // 清理

Composition API中的生命周期钩子

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => {
      console.log('挂载前');
    });

    onMounted(() => {
      console.log('已挂载');
      // DOM可用
      // 设置事件监听器,获取数据
    });

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

    onUpdated(() => {
      console.log('已更新');
      // DOM已更新
    });

    onBeforeUnmount(() => {
      console.log('卸载前');
      // 卸载前清理
    });

    onUnmounted(() => {
      console.log('已卸载');
      // 最终清理
    });

    onErrorCaptured((err, instance, info) => {
      console.error('捕获错误:', err, info);
      return false; // 停止传播
    });

    // 对于包裹在<KeepAlive>中的组件
    onActivated(() => {
      console.log('组件已激活');
    });

    onDeactivated(() => {
      console.log('组件已停用');
    });
  }
};

Composables - 可复用的组合函数

简单Composable

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

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

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  function reset() {
    count.value = initialValue;
  }

  return {
    count: readonly(count),
    doubled,
    increment,
    decrement,
    reset
  };
}

// 使用
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';

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

高级Composable,带副作用

// composables/useFetch.ts
import { ref, unref, watchEffect } from 'vue';
import type { Ref } from 'vue';

export function useFetch<T>(url: Ref<string> | string) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);

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

    try {
      const response = await fetch(unref(url));
      if (!response.ok) throw new Error('获取失败');
      data.value = await response.json();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  }

  watchEffect(() => {
    fetchData();
  });

  return {
    data: readonly(data),
    error: readonly(error),
    loading: readonly(loading),
    refetch: fetchData
  };
}

// 使用
<script setup lang="ts">
import { ref } from 'vue';
import { useFetch } from '@/composables/useFetch';

const userId = ref('1');
const url = computed(() => `/api/users/${userId.value}`);
const { data, error, loading, refetch } = useFetch(url);
</script>

Composable,带清理

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

export function useEventListener(
  target: EventTarget,
  event: string,
  handler: (e: Event) => void
) {
  onMounted(() => {
    target.addEventListener(event, handler);
  });

  onUnmounted(() => {
    target.removeEventListener(event, handler);
  });
}

// 使用
<script setup lang="ts">
import { useEventListener } from '@/composables/useEventListener';

useEventListener(window, 'resize', () => {
  console.log('窗口已调整大小');
});
</script>

Composition API中的Props和Emits

TypeScript Props

<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
  items: string[];
  user: {
    name: string;
    email: string;
  };
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
});

// 访问props
console.log(props.title);
console.log(props.count);

// 解构会失去响应性 - 使用toRefs
import { toRefs } from 'vue';
const { title, count } = toRefs(props);
</script>

TypeScript Emits

<script setup lang="ts">
// 类型安全的emits
const emit = defineEmits<{
  update: [value: number];
  delete: [];
  change: [id: string, value: string];
}>();

function handleUpdate() {
  emit('update', 42);
}

function handleChange(id: string, value: string) {
  emit('change', id, value);
}
</script>

运行时Props验证

<script setup lang="ts">
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value: number) => value >= 0
  },
  status: {
    type: String as PropType<'active' | 'inactive'>,
    default: 'active'
  }
});
</script>

Provide和Inject模式

基础Provide/Inject

<!-- 父组件 -->
<script setup lang="ts">
import { provide, ref } from 'vue';

const theme = ref('dark');
const updateTheme = (newTheme: string) => {
  theme.value = newTheme;
};

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

<!-- 子组件(任何深度) -->
<script setup lang="ts">
import { inject } from 'vue';

const themeContext = inject('theme');
// themeContext.theme
// themeContext.updateTheme('light')
</script>

类型安全的Provide/Inject

// keys.ts
import type { InjectionKey, Ref } from 'vue';

export interface ThemeContext {
  theme: Ref<string>;
  updateTheme: (theme: string) => void;
}

export const ThemeKey: InjectionKey<ThemeContext> =
  Symbol('theme');

// 提供者
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ThemeKey } from './keys';

const theme = ref('dark');
const updateTheme = (newTheme: string) => {
  theme.value = newTheme;
};

provide(ThemeKey, { theme, updateTheme });
</script>

// 消费者
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from './keys';

const theme = inject(ThemeKey);
// 完全类型化!
</script>

带默认值的Provide

<script setup lang="ts">
import { inject } from 'vue';

const theme = inject('theme', {
  theme: ref('light'),
  updateTheme: () => {}
});

// 或使用工厂函数用于响应式默认值
const config = inject('config', () => reactive({
  locale: 'en',
  timezone: 'UTC'
}), true); // true = 视为工厂函数
</script>

TypeScript与Composition API

完整类型的组件

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

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

interface Props {
  userId: number;
}

interface Emits {
  (e: 'update', user: User): void;
  (e: 'delete', id: number): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

const user: Ref<User | null> = ref(null);
const isLoading = ref(false);

const userName: ComputedRef<string> = computed(() =>
  user.value?.name ?? 'Unknown'
);

async function loadUser() {
  isLoading.value = true;
  try {
    const response = await fetch(`/api/users/${props.userId}`);
    user.value = await response.json();
  } finally {
    isLoading.value = false;
  }
}

function updateUser(updates: Partial<User>) {
  if (user.value) {
    user.value = { ...user.value, ...updates };
    emit('update', user.value);
  }
}
</script>

通用Composables

// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue';

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
): Ref<T> {
  const data = ref<T>(defaultValue) as Ref<T>;

  // 从localStorage加载
  const stored = localStorage.getItem(key);
  if (stored) {
    try {
      data.value = JSON.parse(stored);
    } catch (e) {
      console.error('解析localStorage失败', e);
    }
  }

  // 更改时保存到localStorage
  watch(
    data,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return data;
}

// 使用
const user = useLocalStorage<User>('user', { id: 0, name: '' });

何时使用此技能

使用vue-composition-api当构建现代、生产就绪的应用程序时,需要:

  • 复杂组件逻辑,受益于更好的组织
  • 跨多个组件的可复用逻辑(composables)
  • 更好的TypeScript集成和类型推断
  • 细粒度响应性控制
  • 需要可维护性的大规模应用程序
  • 从Vue 2 Options API迁移到Vue 3
  • 无需mixins共享有状态逻辑

Vue特定最佳实践

  1. 优先使用<script setup>语法 - 更简洁,更好性能,更好类型
  2. 使用composables用于可复用逻辑 - 提取到composables/目录
  3. 使用ref用于原始值,reactive用于对象 - 除非需要替换对象
  4. 始终使用TypeScript - 更好开发体验和更少运行时错误
  5. 使用toRefs解构响应式对象 - 保持响应性
  6. 使用computed用于派生状态 - 不要在模板中使用方法
  7. 清理副作用 - 使用onUnmounted用于事件监听器、定时器
  8. 保持组件专注 - 将复杂逻辑提取到composables
  9. 使用provide/inject用于深层prop传递 - 避免prop drilling
  10. 使用use前缀命名composables - 遵循约定(useCounter, useFetch)

Vue特定陷阱

  1. 直接解构props - 失去响应性,使用toRefs(props)
  2. 忘记refs上的.value - 常见错误来源
  3. 修改props - props是只读的,应发出事件
  4. 对整个状态使用reactive() - 无法替换,对根使用ref
  5. 未清理watchers - 内存泄漏,存储停止句柄
  6. 挂载前访问refs - DOM refs在setup中为null
  7. 过度使用reactive() - 对简单值使用ref
  8. 未使用computed用于派生状态 - 每次渲染重新计算
  9. 忘记从setup()返回 - 没有<script setup>
  10. 混合Options API和Composition API - 混淆,选择一个

常见模式

表单处理

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

interface FormData {
  name: string;
  email: string;
  password: string;
}

interface FormErrors {
  name?: string;
  email?: string;
  password?: string;
}

const form = reactive<FormData>({
  name: '',
  email: '',
  password: ''
});

const errors = reactive<FormErrors>({});

const isValid = computed(() =>
  Object.keys(errors).length === 0 &&
  form.name && form.email && form.password
);

function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validate() {
  if (!form.name) {
    errors.name = '姓名必填';
  } else {
    delete errors.name;
  }

  if (!validateEmail(form.email)) {
    errors.email = '无效邮箱';
  } else {
    delete errors.email;
  }

  if (form.password.length < 8) {
    errors.password = '密码必须8+字符';
  } else {
    delete errors.password;
  }
}

async function submit() {
  validate();
  if (!isValid.value) return;

  // 提交表单
  await fetch('/api/register', {
    method: 'POST',
    body: JSON.stringify(form)
  });
}
</script>

异步数据加载

<script setup lang="ts">
import { ref, onMounted } from 'vue';

interface Data {
  id: number;
  title: string;
}

const data = ref<Data[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);

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

  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error('获取失败');
    data.value = await response.json();
  } catch (e) {
    error.value = (e as Error).message;
  } finally {
    loading.value = false;
  }
}

onMounted(() => {
  fetchData();
});
</script>

<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error }}</div>
    <div v-else>
      <div v-for="item in data" :key="item.id">
        {{ item.title }}
      </div>
    </div>
  </div>
</template>

资源