Vue.js Composition API 介绍

Composition API 是 Vue 3 引入的一套新的 API,它提供了更好的逻辑组织和代码复用能力。与 Options API 相比,Composition API 更加灵活,特别适合处理复杂组件和构建大型应用。

1. 什么是 Composition API?

Composition API 是 Vue 3 引入的一套新的 API 风格,它允许开发者使用函数式的方式组织和复用组件逻辑。与 Options API 不同,Composition API 基于逻辑关注点而不是选项类型来组织代码。

1
导入函数

从 Vue 导入需要的函数

2
定义逻辑

在 setup() 中组织逻辑

3
返回数据

返回模板需要的数据和方法

Options API

// Options API 示例
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    };
  },
  computed: {
    doubledCount() {
      return this.count * 2;
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    updateMessage(newMsg) {
      this.message = newMsg;
    }
  },
  mounted() {
    console.log('组件已挂载');
  }
};
                                    
Composition API

// Composition API 示例
import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    // 响应式数据
    const count = ref(0);
    const message = ref('Hello');

    // 计算属性
    const doubledCount = computed(() => count.value * 2);

    // 方法
    const increment = () => {
      count.value++;
    };

    const updateMessage = (newMsg) => {
      message.value = newMsg;
    };

    // 生命周期钩子
    onMounted(() => {
      console.log('组件已挂载');
    });

    // 返回模板需要的内容
    return {
      count,
      message,
      doubledCount,
      increment,
      updateMessage
    };
  }
};
                                    

更好的逻辑组织

相关逻辑可以组织在一起,而不是分散在不同的选项中

更好的代码复用

逻辑可以提取为可重用的组合式函数

更好的类型推断

对 TypeScript 有更好的支持

更小的打包体积

Tree-shaking 友好,只导入需要的函数

2. 为什么需要 Composition API?

Options API 的局限性

逻辑分散问题

在 Options API 中,相关逻辑被分散到不同的选项中:

  • 数据在 data()
  • 方法在 methods
  • 计算属性在 computed
  • 侦听器在 watch
  • 生命周期钩子在各自的位置

当组件变得复杂时,理解和维护变得困难。

代码复用问题

Options API 主要通过以下方式复用代码:

  • Mixin: 容易造成命名冲突
  • 高阶组件: 需要额外的组件层级
  • 渲染函数: 学习曲线陡峭

这些方式都有各自的局限性和缺点。

Composition API 的优势

逻辑关注点对比
优势分析
Options API - 逻辑分散

// 用户相关逻辑 - 分散在各处
export default {
  data() {
    return {
      // 用户数据
      user: null,
      isLoadingUser: false,

      // 文章数据
      posts: [],
      isLoadingPosts: false
    };
  },

  computed: {
    // 用户计算属性
    userName() {
      return this.user?.name || '游客';
    },

    // 文章计算属性
    postCount() {
      return this.posts.length;
    }
  },

  methods: {
    // 用户方法
    async fetchUser() {
      this.isLoadingUser = true;
      this.user = await api.getUser();
      this.isLoadingUser = false;
    },

    // 文章方法
    async fetchPosts() {
      this.isLoadingPosts = true;
      this.posts = await api.getPosts();
      this.isLoadingPosts = false;
    }
  },

  watch: {
    // 用户侦听器
    'user.id'(newId) {
      if (newId) {
        this.fetchPosts();
      }
    }
  },

  mounted() {
    // 用户生命周期
    this.fetchUser();
  }
};
                                            
Composition API - 逻辑集中

// 用户相关逻辑 - 集中在一起
import { ref, computed, watch, onMounted } from 'vue';

export default {
  setup() {
    // ========== 用户逻辑 ==========
    const user = ref(null);
    const isLoadingUser = ref(false);

    const userName = computed(() => user.value?.name || '游客');

    const fetchUser = async () => {
      isLoadingUser.value = true;
      user.value = await api.getUser();
      isLoadingUser.value = false;
    };

    // 用户相关生命周期
    onMounted(fetchUser);

    // ========== 文章逻辑 ==========
    const posts = ref([]);
    const isLoadingPosts = ref(false);

    const postCount = computed(() => posts.value.length);

    const fetchPosts = async () => {
      isLoadingPosts.value = true;
      posts.value = await api.getPosts();
      isLoadingPosts.value = false;
    };

    // 文章相关侦听器
    watch(
      () => user.value?.id,
      (newId) => {
        if (newId) {
          fetchPosts();
        }
      }
    );

    return {
      // 用户相关
      user,
      isLoadingUser,
      userName,
      fetchUser,

      // 文章相关
      posts,
      isLoadingPosts,
      postCount,
      fetchPosts
    };
  }
};
                                            

关键优势:

  • 逻辑集中: 相关功能(用户、文章)的逻辑组织在一起
  • 易于提取: 可以将用户逻辑或文章逻辑提取为独立的组合式函数
  • 更好的可读性: 每个功能的代码都在一个地方,易于理解和维护
  • 类型安全: 更好的 TypeScript 支持
适用场景: Composition API 特别适合以下场景:复杂组件、需要大量逻辑复用的应用、TypeScript 项目、需要更好代码组织的团队项目。

3. setup() 函数

setup() 函数是 Composition API 的入口点。它在组件创建之前执行,用于定义响应式数据、计算属性、方法等。

setup() 函数基础
核心概念

import { ref, computed } from 'vue';

export default {
  // setup 函数在组件创建之前执行
  setup() {
    // 定义响应式数据
    const count = ref(0);
    const message = ref('Hello Vue 3');

    // 定义计算属性
    const reversedMessage = computed(() => {
      return message.value.split('').reverse().join('');
    });

    // 定义方法
    const increment = () => {
      count.value++;
    };

    const updateMessage = (newMsg) => {
      message.value = newMsg;
    };

    // 返回的数据和方法可以在模板中使用
    return {
      count,
      message,
      reversedMessage,
      increment,
      updateMessage
    };
  },

  // 模板中使用
  template: `
    <div>
      <p>计数: {{ count }}</p>
      <button @click="increment">增加</button>

      <p>消息: {{ message }}</p>
      <p>反转消息: {{ reversedMessage }}</p>
      <input :value="message" @input="updateMessage($event.target.value)">
    </div>
  `
};
                                        

import { ref, watch } from 'vue';

export default {
  props: {
    // 定义props
    initialCount: {
      type: Number,
      default: 0
    },
    title: {
      type: String,
      required: true
    }
  },

  emits: ['count-change'], // 定义自定义事件

  // setup 函数接收两个参数
  setup(props, context) {
    // 第一个参数: props - 响应式的props对象
    console.log('props:', props);
    console.log('initialCount:', props.initialCount);
    console.log('title:', props.title);

    // props是响应式的,可以使用watch监听
    watch(
      () => props.initialCount,
      (newVal, oldVal) => {
        console.log(`initialCount从${oldVal}变为${newVal}`);
      }
    );

    // 定义响应式数据,基于props
    const count = ref(props.initialCount);

    // 第二个参数: context - 上下文对象
    // context包含以下属性:
    // - attrs: 非响应式的属性对象
    // - slots: 插槽对象
    // - emit: 触发事件的函数
    // - expose: 暴露公共属性的函数

    const increment = () => {
      count.value++;
      // 使用context.emit触发自定义事件
      context.emit('count-change', count.value);
    };

    // 返回模板需要的内容
    return {
      count,
      increment,
      title: props.title // 可以直接返回props中的值
    };
  }
};
                                        

import { ref } from 'vue';

export default {
  setup(props, context) {
    // context.attrs - 包含所有非props的属性
    console.log('attrs:', context.attrs);
    // 例如: <MyComponent id="my-id" class="my-class" data-test="test" />
    // id、class、data-test都会在attrs中

    // context.slots - 包含所有插槽
    console.log('slots:', context.slots);
    // 可以访问具名插槽和默认插槽
    // context.slots.default() - 默认插槽内容
    // context.slots.header() - 名为header的插槽内容

    // context.emit - 触发自定义事件
    const handleClick = () => {
      context.emit('custom-event', { data: 'some data' });
    };

    // context.expose - 暴露公共属性给父组件
    const internalData = ref('内部数据');

    // 暴露给父组件的内容
    context.expose({
      getInternalData: () => internalData.value,
      resetData: () => {
        internalData.value = '重置后的数据';
      }
    });

    // 只有通过expose暴露的内容才能被父组件访问
    // 父组件可以通过ref访问: this.$refs.myComponent.getInternalData()

    return {
      handleClick
    };
  }
};
                                        

<!-- 使用 <script setup> 语法糖 -->
<script setup>
// 这个脚本会在组件每次创建时执行
// 所有内容都会自动暴露给模板

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

// 定义props
const props = defineProps({
  initialCount: {
    type: Number,
    default: 0
  },
  title: String
});

// 定义emit
const emit = defineEmits(['count-change']);

// 响应式数据
const count = ref(props.initialCount);
const message = ref('Hello Vue 3');

// 计算属性
const doubledCount = computed(() => count.value * 2);

// 方法
const increment = () => {
  count.value++;
  emit('count-change', count.value);
};

const updateMessage = (newMsg) => {
  message.value = newMsg;
};

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载');
});

// 不需要return,所有内容自动可用
</script>

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

    <p>消息: {{ message }}</p>
    <input :value="message" @input="updateMessage($event.target.value)">
  </div>
</template>

<style scoped>
/* 样式 */
</style>
                                        
<script setup> 优势: 语法更简洁,不需要手动返回变量,自动推断类型,更好的 IDE 支持。这是 Composition API 的推荐写法。

setup() 函数执行时机

setup()
组件创建前执行
beforeCreate
在 setup() 之前
created
在 setup() 之后
beforeMount
挂载之前
mounted
挂载之后
重要:setup() 函数中,无法访问 this。组件实例在 setup() 执行时尚未创建。所有组件选项(如 data、methods 等)都需要在 setup() 中定义。

4. 响应式 API

Composition API 提供了两种创建响应式数据的方式:ref()reactive()

ref() - 创建响应式引用

用途: 创建单个值的响应式引用

特点:

  • 可以包装任何类型的值
  • 通过 .value 访问和修改值
  • 在模板中自动解包,不需要 .value
  • 适合原始类型和对象引用

import { ref } from 'vue';

const count = ref(0); // 原始类型
const user = ref({ name: 'John' }); // 对象

// 修改值
count.value = 1;
user.value.name = 'Jane';

// 在模板中使用
// <p>{{ count }}</p> 自动解包,不需要 .value
                                        
reactive() - 创建响应式对象

用途: 创建响应式的对象或数组

特点:

  • 只能用于对象类型(对象、数组、Map、Set)
  • 直接访问和修改属性,不需要 .value
  • 返回的是对象的代理(Proxy)
  • 适合复杂的数据结构

import { reactive } from 'vue';

const state = reactive({
  count: 0,
  user: { name: 'John' },
  items: ['item1', 'item2']
});

// 直接修改属性
state.count = 1;
state.user.name = 'Jane';
state.items.push('item3');

// 在模板中使用
// <p>{{ state.count }}</p>
                                        
ref() vs reactive() 对比
核心概念
特性 ref() reactive()
包装类型 任何类型 仅对象类型
访问方式 通过 .value 属性 直接访问属性
模板使用 自动解包,不需要 .value 直接访问属性
解构/展开 使用 toRefs() 保持响应性 解构会失去响应性
TypeScript 支持 类型推断良好 类型推断良好
适用场景 原始值、单个值、需要明确引用的场景 复杂对象、表单数据、状态对象

其他响应式工具

toRef()

为响应式对象的属性创建 ref


import { reactive, toRef } from 'vue';

const state = reactive({ count: 0 });
const countRef = toRef(state, 'count');

// countRef 是 state.count 的引用
countRef.value++; // state.count 也会更新
                                        
toRefs()

将响应式对象转换为普通对象,每个属性都是 ref


import { reactive, toRefs } from 'vue';

const state = reactive({
  count: 0,
  name: 'Vue'
});

// 解构时保持响应性
const { count, name } = toRefs(state);

// 在模板中可以直接使用 count, name
                                        
readonly()

创建只读的响应式对象


import { reactive, readonly } from 'vue';

const original = reactive({ count: 0 });
const copy = readonly(original);

// copy.count++ // 错误: 只读属性
original.count++; // 可以修改原始对象
                                        
响应式数据实战示例
实战

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

// 使用 ref 创建基本数据
const isLoading = ref(false);
const error = ref(null);

// 使用 reactive 创建复杂状态
const state = reactive({
  user: {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    profile: {
      age: 25,
      city: '北京'
    }
  },
  posts: [
    { id: 1, title: '第一篇帖子', content: '内容...' },
    { id: 2, title: '第二篇帖子', content: '内容...' }
  ],
  settings: {
    theme: 'light',
    notifications: true
  }
});

// 使用 toRefs 解构,保持响应性
const { user, posts, settings } = toRefs(state);

// 计算属性
const postCount = computed(() => posts.value.length);
const fullUserInfo = computed(() => {
  return `${user.value.name} (${user.value.profile.age}岁, ${user.value.profile.city})`;
});

// 方法
const updateUserName = (newName) => {
  user.value.name = newName;
};

const addPost = () => {
  const newId = posts.value.length + 1;
  posts.value.push({
    id: newId,
    title: `新帖子 ${newId}`,
    content: '这是新帖子的内容...'
  });
};

const toggleTheme = () => {
  settings.value.theme = settings.value.theme === 'light' ? 'dark' : 'light';
};

// 异步操作示例
const fetchData = async () => {
  isLoading.value = true;
  error.value = null;

  try {
    // 模拟API调用
    await new Promise(resolve => setTimeout(resolve, 1000));

    // 更新数据
    user.value.name = '更新后的名字';
    posts.value.push({
      id: posts.value.length + 1,
      title: '异步加载的帖子',
      content: '这是异步加载的内容...'
    });
  } catch (err) {
    error.value = err.message;
  } finally {
    isLoading.value = false;
  }
};
</script>

<template>
  <div>
    <h2>用户信息</h2>
    <p>{{ fullUserInfo }}</p>
    <button @click="updateUserName('李四')">更新用户名</button>

    <h3>帖子列表 ({{ postCount }})</h3>
    <ul>
      <li v-for="post in posts" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
    <button @click="addPost">添加帖子</button>

    <h3>设置</h3>
    <p>主题: {{ settings.theme }}</p>
    <button @click="toggleTheme">切换主题</button>

    <div v-if="isLoading">加载中...</div>
    <div v-if="error" class="error">错误: {{ error }}</div>
    <button @click="fetchData" :disabled="isLoading">
      {{ isLoading ? '加载中...' : '获取数据' }}
    </button>
  </div>
</template>
                            
选择建议: 大多数情况下,使用 ref() 就足够了。当你有多个相关属性需要组织在一起时,可以使用 reactive()。记住,解构 reactive() 对象会失去响应性,需要使用 toRefs()

5. 计算属性和侦听器

计算属性 - computed()

基本用法

import { ref, computed } from 'vue';

const count = ref(0);
const price = ref(10);

// 只读计算属性
const total = computed(() => {
  return count.value * price.value;
});

// 在模板中使用
// <p>总价: {{ total }}</p>
                                        
可写计算属性

import { ref, computed } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

// 可写计算属性
const fullName = computed({
  // getter
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  // setter
  set(newValue) {
    const names = newValue.split(' ');
    firstName.value = names[0];
    lastName.value = names[1] || '';
  }
});

// 使用setter
fullName.value = '李 四';
                                        

侦听器 - watch() 和 watchEffect()

watch() - 精确侦听

特点:

  • 惰性执行(默认不会立即执行)
  • 可以指定侦听的数据源
  • 可以获取旧值和新值
  • 支持深度侦听

import { ref, watch } from 'vue';

const count = ref(0);
const user = ref({ name: 'John', age: 25 });

// 侦听单个ref
watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变为${newVal}`);
});

// 侦听getter函数
watch(
  () => user.value.name,
  (newName, oldName) => {
    console.log(`用户名从${oldName}变为${newName}`);
  }
);

// 侦听多个源
watch([count, () => user.value.age],
  ([newCount, newAge], [oldCount, oldAge]) => {
    console.log(`count: ${oldCount}→${newCount}`);
    console.log(`age: ${oldAge}→${newAge}`);
  }
);

// 深度侦听对象
watch(
  user,
  (newUser, oldUser) => {
    console.log('user对象变化:', newUser);
  },
  { deep: true } // 深度侦听
);

// 立即执行
watch(count, (newVal, oldVal) => {
  console.log('立即执行:', newVal);
}, { immediate: true });
                                        
watchEffect() - 自动侦听

特点:

  • 立即执行,自动收集依赖
  • 不需要指定侦听的数据源
  • 无法获取旧值
  • 适合执行副作用

import { ref, watchEffect } from 'vue';

const count = ref(0);
const price = ref(10);

// 自动侦听函数中使用的响应式数据
watchEffect(() => {
  console.log(`watchEffect执行,count=${count.value}`);
  const total = count.value * price.value;
  console.log(`总价: ${total}`);

  // 副作用:更新DOM或调用API
  document.title = `计数: ${count.value}`;
});

// 当count或price变化时,watchEffect会自动重新执行

// 停止侦听
const stop = watchEffect(() => {
  // 侦听逻辑
});

// 手动停止
stop();

// 清理副作用
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('定时器执行');
  }, 1000);

  // 清理函数
  onCleanup(() => {
    clearTimeout(timer);
    console.log('清理定时器');
  });
});
                                        
计算属性和侦听器实战
实战

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

// 表单数据
const form = reactive({
  firstName: '张',
  lastName: '三',
  age: 25,
  email: 'zhangsan@example.com'
});

// 计算属性
const fullName = computed(() => {
  return `${form.firstName} ${form.lastName}`;
});

const isAdult = computed(() => {
  return form.age >= 18;
});

const emailDomain = computed(() => {
  return form.email.split('@')[1] || '';
});

// 使用watch精确侦听
watch(
  () => form.age,
  (newAge, oldAge) => {
    console.log(`年龄从${oldAge}变为${newAge}`);

    if (newAge >= 18 && oldAge < 18) {
      console.log('恭喜!您现在是成年人了。');
    }
  }
);

// 侦听多个字段
watch(
  [() => form.firstName, () => form.lastName],
  ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
    console.log(`姓名从${oldFirstName} ${oldLastName}变为${newFirstName} ${newLastName}`);
  }
);

// 使用watchEffect自动侦听
const stopWatch = watchEffect(() => {
  console.log(`表单数据更新: ${fullName.value}, ${form.age}岁`);

  // 验证逻辑
  if (form.age < 0) {
    console.error('年龄不能为负数');
  }

  if (!form.email.includes('@')) {
    console.error('邮箱格式不正确');
  }
});

// 方法
const updateAge = () => {
  form.age += 1;
};

const resetForm = () => {
  form.firstName = '张';
  form.lastName = '三';
  form.age = 25;
  form.email = 'zhangsan@example.com';
};

// 停止watchEffect
const stopEffect = () => {
  stopWatch();
  console.log('已停止自动侦听');
};
</script>

<template>
  <div>
    <h2>用户信息</h2>
    <p>全名: {{ fullName }}</p>
    <p>年龄: {{ form.age }} <span v-if="isAdult">(成年人)</span></p>
    <p>邮箱: {{ form.email }} <span v-if="emailDomain">(域名: {{ emailDomain }})</span></p>

    <div class="form-group">
      <label>名:</label>
      <input v-model="form.firstName">
    </div>

    <div class="form-group">
      <label>姓:</label>
      <input v-model="form.lastName">
    </div>

    <div class="form-group">
      <label>年龄:</label>
      <input v-model.number="form.age" type="number">
    </div>

    <div class="form-group">
      <label>邮箱:</label>
      <input v-model="form.email" type="email">
    </div>

    <button @click="updateAge">增加年龄</button>
    <button @click="resetForm">重置表单</button>
    <button @click="stopEffect">停止自动侦听</button>
  </div>
</template>
                            
watch() vs watchEffect(): 使用 watch() 当你需要精确控制侦听什么数据以及何时执行;使用 watchEffect() 当你需要自动侦听函数中使用的所有响应式数据,并且需要立即执行副作用。

6. 生命周期钩子

在 Composition API 中,生命周期钩子是通过函数形式导入和使用的。

生命周期钩子对照表
核心概念
Options API Composition API 执行时机
beforeCreate 不需要(在 setup 中替代) 实例初始化之后,数据观测之前
created 不需要(在 setup 中替代) 实例创建完成后
beforeMount onBeforeMount 挂载开始之前
mounted onMounted 挂载完成后
beforeUpdate onBeforeUpdate 数据更新时,DOM 打补丁之前
updated onUpdated 数据更新后,DOM 打补丁之后
beforeUnmount onBeforeUnmount 卸载之前
unmounted onUnmounted 卸载完成后
errorCaptured onErrorCaptured 捕获后代组件错误时
activated onActivated 被 keep-alive 缓存的组件激活时
deactivated onDeactivated 被 keep-alive 缓存的组件停用时

使用示例

基本用法

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

export default {
  setup() {
    const count = ref(0);

    // 挂载阶段
    onBeforeMount(() => {
      console.log('组件即将挂载');
    });

    onMounted(() => {
      console.log('组件已挂载');
      // 可以访问DOM元素
      console.log('DOM元素:', document.getElementById('app'));
    });

    // 更新阶段
    onBeforeUpdate(() => {
      console.log('组件即将更新');
      console.log('当前count:', count.value);
    });

    onUpdated(() => {
      console.log('组件已更新');
      console.log('更新后count:', count.value);
    });

    // 卸载阶段
    onBeforeUnmount(() => {
      console.log('组件即将卸载');
      // 清理工作
    });

    onUnmounted(() => {
      console.log('组件已卸载');
    });

    // 错误捕获
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err);
      console.error('错误信息:', info);
      // 返回false阻止错误继续向上传播
      return false;
    });

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

    return { count, increment };
  }
};
                                        
实战示例

<script setup>
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue';

// 数据
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);

// 定时器
const timer = ref(null);
const seconds = ref(0);

// 事件监听
const mousePosition = ref({ x: 0, y: 0 });

// 组件挂载时
onMounted(async () => {
  console.log('组件挂载,开始获取数据');

  // 获取初始数据
  await fetchData();

  // 启动定时器
  startTimer();

  // 添加事件监听
  window.addEventListener('mousemove', updateMousePosition);
});

// 组件卸载时
onUnmounted(() => {
  console.log('组件卸载,清理资源');

  // 清除定时器
  if (timer.value) {
    clearInterval(timer.value);
  }

  // 移除事件监听
  window.removeEventListener('mousemove', updateMousePosition);
});

// keep-alive 相关
onActivated(() => {
  console.log('组件激活');
  // 重新启动定时器
  startTimer();
});

onDeactivated(() => {
  console.log('组件停用');
  // 暂停定时器
  if (timer.value) {
    clearInterval(timer.value);
    timer.value = null;
  }
});

// 方法
const fetchData = async () => {
  isLoading.value = true;
  error.value = null;

  try {
    // 模拟API调用
    await new Promise(resolve => setTimeout(resolve, 1000));
    data.value = { message: '数据加载成功', timestamp: new Date() };
  } catch (err) {
    error.value = '数据加载失败';
  } finally {
    isLoading.value = false;
  }
};

const startTimer = () => {
  if (timer.value) {
    clearInterval(timer.value);
  }

  timer.value = setInterval(() => {
    seconds.value++;
  }, 1000);
};

const updateMousePosition = (event) => {
  mousePosition.value = {
    x: event.clientX,
    y: event.clientY
  };
};
</script>

<template>
  <div>
    <h3>生命周期示例</h3>

    <div v-if="isLoading">加载中...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <div v-else-if="data">
      <p>{{ data.message }}</p>
      <p>时间: {{ data.timestamp }}</p>
    </div>

    <p>定时器: {{ seconds }} 秒</p>
    <p>鼠标位置: X={{ mousePosition.x }}, Y={{ mousePosition.y }}</p>
  </div>
</template>
                                        
注意: Composition API 的生命周期钩子需要在 setup() 函数中同步调用。不要在异步函数中调用它们,否则可能无法正确注册。如果需要异步操作,可以在钩子内部使用异步函数。

7. 依赖注入

在 Composition API 中,依赖注入通过 provide()inject() 函数实现。

provide() - 提供依赖

import { provide, ref, reactive, computed } from 'vue';

export default {
  setup() {
    // 提供基本值
    provide('appName', '我的应用');

    // 提供响应式数据
    const user = ref({ name: '张三', age: 25 });
    provide('user', user);

    // 提供响应式对象
    const theme = reactive({
      mode: 'dark',
      colors: {
        primary: '#42b983',
        secondary: '#3498db'
      }
    });
    provide('theme', theme);

    // 提供计算属性
    const isDarkMode = computed(() => theme.mode === 'dark');
    provide('isDarkMode', isDarkMode);

    // 提供方法
    const updateTheme = (newMode) => {
      theme.mode = newMode;
    };
    provide('updateTheme', updateTheme);

    return {};
  }
};
                                        
inject() - 注入依赖

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

export default {
  setup() {
    // 注入基本值
    const appName = inject('appName');
    console.log('应用名称:', appName);

    // 注入响应式数据
    const user = inject('user');
    console.log('用户:', user.value);

    // 注入响应式对象
    const theme = inject('theme');
    console.log('主题:', theme);

    // 注入计算属性
    const isDarkMode = inject('isDarkMode');
    console.log('是否为暗黑模式:', isDarkMode.value);

    // 注入方法
    const updateTheme = inject('updateTheme');

    // 提供默认值
    const config = inject('config', { theme: 'light', lang: 'zh' });

    // 如果依赖必须存在
    const requiredDep = inject('requiredDep');
    if (!requiredDep) {
      throw new Error('requiredDep is required');
    }

    // 本地方法
    const toggleTheme = () => {
      if (updateTheme) {
        updateTheme(theme.mode === 'dark' ? 'light' : 'dark');
      }
    };

    return {
      appName,
      user,
      theme,
      isDarkMode,
      config,
      toggleTheme
    };
  }
};
                                        
依赖注入实战示例
实战

<!-- 根组件: App.vue -->
<script setup>
import { provide, ref, reactive, computed } from 'vue';
import ChildComponent from './ChildComponent.vue';

// 应用主题
const theme = reactive({
  mode: 'light',
  colors: {
    primary: '#42b983',
    secondary: '#3498db',
    background: '#ffffff',
    text: '#333333'
  }
});

// 用户信息
const currentUser = ref({
  id: 1,
  name: '张三',
  role: 'admin',
  preferences: {
    notifications: true,
    language: 'zh-CN'
  }
});

// 应用配置
const appConfig = reactive({
  version: '1.0.0',
  apiUrl: 'https://api.example.com',
  features: {
    darkMode: true,
    multiLanguage: true,
    analytics: false
  }
});

// 计算属性
const isAuthenticated = computed(() => !!currentUser.value.id);
const isAdmin = computed(() => currentUser.value.role === 'admin');

// 方法
const toggleTheme = () => {
  theme.mode = theme.mode === 'light' ? 'dark' : 'light';

  // 更新颜色
  if (theme.mode === 'dark') {
    theme.colors.background = '#1a1a1a';
    theme.colors.text = '#ffffff';
  } else {
    theme.colors.background = '#ffffff';
    theme.colors.text = '#333333';
  }
};

const updateUser = (newUserData) => {
  Object.assign(currentUser.value, newUserData);
};

// 提供依赖
provide('theme', theme);
provide('currentUser', currentUser);
provide('appConfig', appConfig);
provide('isAuthenticated', isAuthenticated);
provide('isAdmin', isAdmin);
provide('toggleTheme', toggleTheme);
provide('updateUser', updateUser);
</script>

<template>
  <div :style="{
    backgroundColor: theme.colors.background,
    color: theme.colors.text,
    padding: '20px',
    minHeight: '100vh'
  }">
    <h1>我的应用</h1>
    <button @click="toggleTheme">
      切换主题 (当前: {{ theme.mode }})
    </button>
    <ChildComponent />
  </div>
</template>

<!-- 子组件: ChildComponent.vue -->
<script setup>
import { inject, computed } from 'vue';
import GrandChildComponent from './GrandChildComponent.vue';

// 注入依赖
const theme = inject('theme');
const currentUser = inject('currentUser');
const appConfig = inject('appConfig');
const isAuthenticated = inject('isAuthenticated');
const isAdmin = inject('isAdmin');
const toggleTheme = inject('toggleTheme');
const updateUser = inject('updateUser');

// 本地计算属性
const userInfo = computed(() => {
  return `${currentUser.value.name} (${currentUser.value.role})`;
});

const themeInfo = computed(() => {
  return `主题: ${theme.mode}, 主色: ${theme.colors.primary}`;
});

// 本地方法
const updateUserName = () => {
  const newName = prompt('请输入新用户名:', currentUser.value.name);
  if (newName) {
    updateUser({ name: newName });
  }
};
</script>

<template>
  <div style="margin: 20px 0; padding: 15px; border: 1px solid #ccc;">
    <h2>子组件</h2>

    <div>
      <p>{{ userInfo }}</p>
      <p>认证状态: {{ isAuthenticated ? '已认证' : '未认证' }}</p>
      <p>管理员: {{ isAdmin ? '是' : '否' }}</p>
      <button @click="updateUserName">更新用户名</button>
    </div>

    <div>
      <p>{{ themeInfo }}</p>
      <button @click="toggleTheme">切换主题</button>
    </div>

    <div>
      <p>应用版本: {{ appConfig.version }}</p>
      <p>API地址: {{ appConfig.apiUrl }}</p>
    </div>

    <!-- 孙子组件 -->
    <GrandChildComponent />
  </div>
</template>

<!-- 孙子组件: GrandChildComponent.vue -->
<script setup>
import { inject } from 'vue';

// 注入依赖(可以跳过中间组件)
const theme = inject('theme');
const currentUser = inject('currentUser');
const toggleTheme = inject('toggleTheme');
const isAdmin = inject('isAdmin', false); // 提供默认值
</script>

<template>
  <div style="margin: 15px 0; padding: 10px; background-color: rgba(0,0,0,0.05);">
    <h3>孙子组件</h3>
    <p>当前用户: {{ currentUser.name }}</p>
    <p>主题模式: {{ theme.mode }}</p>
    <p>是否是管理员: {{ isAdmin ? '是' : '否' }}</p>
    <button @click="toggleTheme">从孙子组件切换主题</button>
  </div>
</template>
                            
使用建议: 对于全局状态(如主题、用户信息、配置等),使用依赖注入非常合适。对于组件特定的状态,应该使用 props 或本地状态。记住,过度使用依赖注入会使组件间的依赖关系变得隐式,降低代码的可维护性。

8. 模板引用

在 Composition API 中,模板引用通过 ref() 函数创建,这与响应式 ref() 是同一个函数。

模板引用基础
核心概念

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

// 创建模板引用
const inputRef = ref(null); // 初始值为null
const divRef = ref(null);
const buttonRef = ref(null);

// 组件挂载后,引用会被自动填充
onMounted(() => {
  console.log('input元素:', inputRef.value);
  console.log('div元素:', divRef.value);
  console.log('button元素:', buttonRef.value);

  // 可以立即操作元素
  if (inputRef.value) {
    inputRef.value.focus();
  }
});

// 方法
const focusInput = () => {
  if (inputRef.value) {
    inputRef.value.focus();
    inputRef.value.select();
  }
};

const changeDivColor = () => {
  if (divRef.value) {
    divRef.value.style.backgroundColor = '#ffeb3b';
    divRef.value.style.padding = '20px';
  }
};

const simulateClick = () => {
  if (buttonRef.value) {
    buttonRef.value.click();
  }
};
</script>

<template>
  <div>
    <!-- 绑定引用 -->
    <input ref="inputRef" type="text" placeholder="请输入内容">
    <button @click="focusInput">聚焦输入框</button>

    <div ref="divRef" style="padding: 10px; border: 1px solid #ccc; margin: 10px 0;">
      这是一个可修改样式的div
    </div>
    <button @click="changeDivColor">修改div样式</button>

    <button ref="buttonRef" @click="alert('按钮被点击了!')">
      原始按钮
    </button>
    <button @click="simulateClick">模拟点击原始按钮</button>
  </div>
</template>
                                        

<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';

// 引用子组件
const childRef = ref(null);

onMounted(() => {
  console.log('子组件实例:', childRef.value);

  // 访问子组件的公共方法
  if (childRef.value) {
    console.log('子组件数据:', childRef.value.getData());

    // 调用子组件方法
    childRef.value.sayHello();
  }
});

// 调用子组件方法
const callChildMethod = () => {
  if (childRef.value) {
    childRef.value.increment();
  }
};

const getChildData = () => {
  if (childRef.value) {
    const data = childRef.value.getData();
    alert(`子组件数据: count=${data.count}, message=${data.message}`);
  }
};
</script>

<template>
  <div>
    <h3>父组件</h3>
    <button @click="callChildMethod">调用子组件方法</button>
    <button @click="getChildData">获取子组件数据</button>

    <!-- 引用子组件 -->
    <ChildComponent ref="childRef" />
  </div>
</template>

<!-- 子组件: ChildComponent.vue -->
<script setup>
import { ref, defineExpose } from 'vue';

const count = ref(0);
const message = ref('Hello from child');

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

const sayHello = () => {
  alert('Hello!');
};

const getData = () => {
  return {
    count: count.value,
    message: message.value
  };
};

// 暴露给父组件的内容
defineExpose({
  increment,
  sayHello,
  getData,
  count, // 也可以暴露响应式数据(不推荐)
  message
});
</script>

<template>
  <div style="padding: 20px; border: 2px solid #42b983; margin: 10px 0;">
    <h4>子组件</h4>
    <p>计数: {{ count }}</p>
    <p>消息: {{ message }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>
                                        

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

// 模板引用
const elementRef = ref(null);
const inputRef = ref(null);

// 响应式数据
const elementInfo = ref('元素未加载');
const inputValue = ref('');

// 监听引用变化
watch(elementRef, (newElement, oldElement) => {
  console.log('元素引用变化:', { old: oldElement, new: newElement });

  if (newElement) {
    elementInfo.value = `元素已加载,标签名: ${newElement.tagName}`;

    // 可以立即操作元素
    newElement.style.border = '2px solid #3498db';
    newElement.style.padding = '10px';
  } else if (oldElement) {
    elementInfo.value = '元素已卸载';
  }
}, {
  immediate: true // 立即执行一次
});

// 监听输入框引用变化
watch(inputRef, (newInput) => {
  if (newInput) {
    console.log('输入框已加载');
    newInput.focus();

    // 监听输入框输入事件
    newInput.addEventListener('input', (event) => {
      inputValue.value = event.target.value;
    });
  }
});

// 动态修改样式
const changeElementStyle = () => {
  if (elementRef.value) {
    const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'];
    const randomColor = colors[Math.floor(Math.random() * colors.length)];

    elementRef.value.style.backgroundColor = randomColor;
    elementRef.value.style.color = 'white';
    elementRef.value.style.fontWeight = 'bold';
  }
};

// 清除输入框
const clearInput = () => {
  if (inputRef.value) {
    inputRef.value.value = '';
    inputRef.value.focus();
    inputValue.value = '';
  }
};
</script>

<template>
  <div>
    <h3>响应式模板引用</h3>

    <div ref="elementRef" style="margin: 10px 0; padding: 15px;">
      <p>{{ elementInfo }}</p>
    </div>
    <button @click="changeElementStyle">修改元素样式</button>

    <div style="margin: 20px 0;">
      <input ref="inputRef" type="text" placeholder="输入内容...">
      <button @click="clearInput" style="margin-left: 10px;">清除</button>
    </div>

    <p>输入的值: {{ inputValue }}</p>
  </div>
</template>
                                        

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

// 项目列表
const items = ref([
  { id: 1, name: '项目1' },
  { id: 2, name: '项目2' },
  { id: 3, name: '项目3' },
  { id: 4, name: '项目4' }
]);

// 引用数组
const itemRefs = ref([]);

// 动态添加项目
const addItem = () => {
  const newId = items.value.length + 1;
  items.value.push({ id: newId, name: `项目${newId}` });
};

// 移除项目
const removeItem = (index) => {
  items.value.splice(index, 1);
};

// 高亮指定项目
const highlightItem = (index) => {
  if (itemRefs.value[index]) {
    // 先清除所有高亮
    clearHighlights();

    // 高亮指定项目
    const element = itemRefs.value[index];
    element.style.backgroundColor = '#ffeb3b';
    element.style.fontWeight = 'bold';
  }
};

// 清除所有高亮
const clearHighlights = () => {
  itemRefs.value.forEach(element => {
    if (element) {
      element.style.backgroundColor = '';
      element.style.fontWeight = '';
    }
  });
};

// 获取所有项目文本
const getAllItemText = () => {
  const texts = itemRefs.value
    .filter(element => element)
    .map(element => element.textContent.trim());

  alert(`所有项目: ${texts.join(', ')}`);
};

// 设置引用函数
const setItemRef = (el) => {
  if (el) {
    itemRefs.value.push(el);
  }
};

// 组件挂载后
onMounted(() => {
  console.log('项目引用数组:', itemRefs.value);
  console.log('项目数量:', itemRefs.value.length);
});
</script>

<template>
  <div>
    <h3>v-for 中的模板引用</h3>

    <button @click="addItem" style="margin-right: 10px;">添加项目</button>
    <button @click="clearHighlights" style="margin-right: 10px;">清除高亮</button>
    <button @click="getAllItemText">获取所有项目文本</button>

    <ul style="margin-top: 20px;">
      <li v-for="(item, index) in items"
          :key="item.id"
          :ref="setItemRef"
          style="padding: 10px; margin: 5px 0; border: 1px solid #ccc;">
        {{ item.name }}
        <button @click="highlightItem(index)" style="margin-left: 10px;">高亮此项</button>
        <button @click="removeItem(index)" style="margin-left: 5px;">移除</button>
      </li>
    </ul>

    <p>项目总数: {{ items.length }}</p>
  </div>
</template>
                                        

9. 组合式函数

组合式函数(Composables)是使用 Composition API 封装的可复用逻辑函数。这是 Composition API 最强大的功能之一。

创建组合式函数
核心功能

// useCounter.js - 计数器组合式函数
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  // 状态
  const count = ref(initialValue);

  // 计算属性
  const doubled = computed(() => count.value * 2);
  const isEven = computed(() => count.value % 2 === 0);
  const isOdd = computed(() => !isEven.value);

  // 方法
  const increment = (step = 1) => {
    count.value += step;
  };

  const decrement = (step = 1) => {
    count.value -= step;
  };

  const reset = () => {
    count.value = initialValue;
  };

  const setValue = (newValue) => {
    count.value = newValue;
  };

  // 返回状态和方法
  return {
    // 状态
    count,

    // 计算属性
    doubled,
    isEven,
    isOdd,

    // 方法
    increment,
    decrement,
    reset,
    setValue
  };
}

// 在组件中使用
<script setup>
import { useCounter } from './useCounter';

// 使用组合式函数
const counter = useCounter(10);

// 也可以解构使用
const { count, increment, reset } = useCounter(5);
</script>

<template>
  <div>
    <p>计数: {{ counter.count }}</p>
    <p>双倍: {{ counter.doubled }}</p>
    <p>是否为偶数: {{ counter.isEven ? '是' : '否' }}</p>

    <button @click="counter.increment()">增加</button>
    <button @click="counter.decrement()">减少</button>
    <button @click="counter.reset()">重置</button>
    <button @click="counter.setValue(100)">设为100</button>
  </div>
</template>
                                        

// useMouse.js - 鼠标跟踪组合式函数
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  // 状态
  const x = ref(0);
  const y = ref(0);

  // 计算属性
  const position = computed(() => ({ x: x.value, y: y.value }));

  // 更新鼠标位置
  const update = (event) => {
    x.value = event.clientX;
    y.value = event.clientY;
  };

  // 添加事件监听
  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  // 移除事件监听
  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  // 返回状态和方法
  return {
    x,
    y,
    position,
    update // 也可以暴露update方法
  };
}

// 增强版: 带选项的useMouse
export function useMouseWithOptions(options = {}) {
  const { throttleDelay = 0 } = options;

  const x = ref(0);
  const y = ref(0);

  let throttleTimer = null;

  const update = (event) => {
    if (throttleDelay > 0) {
      if (throttleTimer) return;

      throttleTimer = setTimeout(() => {
        x.value = event.clientX;
        y.value = event.clientY;
        throttleTimer = null;
      }, throttleDelay);
    } else {
      x.value = event.clientX;
      y.value = event.clientY;
    }
  };

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
    if (throttleTimer) {
      clearTimeout(throttleTimer);
    }
  });

  return { x, y };
}

// 在组件中使用
<script setup>
import { useMouse, useMouseWithOptions } from './useMouse';

// 基本用法
const { x, y } = useMouse();

// 带选项的用法
const { x: throttledX, y: throttledY } = useMouseWithOptions({
  throttleDelay: 100 // 100ms节流
});
</script>

<template>
  <div>
    <h3>鼠标跟踪</h3>
    <p>鼠标位置: X={{ x }}, Y={{ y }}</p>
    <p>节流位置: X={{ throttledX }}, Y={{ throttledY }}</p>

    <div style="height: 300px; border: 1px solid #ccc; margin-top: 20px;">
      在此区域移动鼠标
    </div>
  </div>
</template>
                                        

// useFetch.js - 数据获取组合式函数
import { ref, watch, computed } from 'vue';

export function useFetch(url, options = {}) {
  // 状态
  const data = ref(null);
  const error = ref(null);
  const isLoading = ref(false);

  // 配置
  const {
    immediate = true,
    manual = false,
    onSuccess,
    onError,
    transform
  } = options;

  // 计算属性
  const hasError = computed(() => !!error.value);
  const hasData = computed(() => !!data.value);
  const isEmpty = computed(() =>
    Array.isArray(data.value) ? data.value.length === 0 : !data.value
  );

  // 获取数据
  const execute = async (executeUrl = url, executeOptions = {}) => {
    isLoading.value = true;
    error.value = null;

    try {
      const response = await fetch(executeUrl, executeOptions);

      if (!response.ok) {
        throw new Error(`HTTP错误: ${response.status}`);
      }

      let result = await response.json();

      // 数据转换
      if (transform && typeof transform === 'function') {
        result = transform(result);
      }

      data.value = result;

      // 成功回调
      if (onSuccess) {
        onSuccess(result);
      }

      return result;
    } catch (err) {
      error.value = err.message || '未知错误';

      // 错误回调
      if (onError) {
        onError(err);
      }

      throw err;
    } finally {
      isLoading.value = false;
    }
  };

  // 重新获取
  const refetch = () => execute();

  // 重置状态
  const reset = () => {
    data.value = null;
    error.value = null;
    isLoading.value = false;
  };

  // 自动获取
  if (immediate && !manual) {
    execute();
  }

  // 监听URL变化
  if (typeof url === 'function' || (options.watch && typeof url !== 'function')) {
    watch(
      () => (typeof url === 'function' ? url() : url),
      (newUrl) => {
        if (newUrl) {
          execute(newUrl);
        }
      },
      { immediate: !manual }
    );
  }

  return {
    // 状态
    data,
    error,
    isLoading,

    // 计算属性
    hasError,
    hasData,
    isEmpty,

    // 方法
    execute,
    refetch,
    reset
  };
}

// 在组件中使用
<script setup>
import { computed } from 'vue';
import { useFetch } from './useFetch';

// 基本用法
const { data: userData, isLoading, error } = useFetch(
  'https://jsonplaceholder.typicode.com/users/1'
);

// 带选项的用法
const { data: posts, refetch } = useFetch(
  () => `https://jsonplaceholder.typicode.com/posts?userId=${userId.value}`,
  {
    manual: true, // 手动执行
    transform: (data) => data.slice(0, 5), // 只取前5条
    onSuccess: (data) => {
      console.log('数据获取成功:', data);
    },
    onError: (err) => {
      console.error('数据获取失败:', err);
    }
  }
);

// 依赖其他数据的获取
const userId = ref(1);
const { data: user } = useFetch(
  () => `https://jsonplaceholder.typicode.com/users/${userId.value}`,
  { watch: true } // 监听userId变化
);

const changeUser = () => {
  userId.value = userId.value === 1 ? 2 : 1;
};
</script>

<template>
  <div>
    <h3>数据获取示例</h3>

    <div v-if="isLoading">加载中...</div>
    <div v-else-if="error" class="error">错误: {{ error }}</div>
    <div v-else-if="userData">
      <p>用户: {{ userData.name }}</p>
      <p>邮箱: {{ userData.email }}</p>
    </div>

    <div>
      <p>当前用户ID: {{ userId }}</p>
      <button @click="changeUser">切换用户</button>
      <button @click="refetch">重新获取数据</button>
    </div>

    <div v-if="user">
      <h4>用户信息</h4>
      <p>{{ user.name }} - {{ user.email }}</p>
    </div>
  </div>
</template>
                                        

// useLocalStorage.js - 本地存储组合式函数
import { ref, watch } from 'vue';

export function useLocalStorage(key, initialValue) {
  // 从localStorage读取初始值
  const readValue = () => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`读取localStorage键"${key}"时出错:`, error);
      return initialValue;
    }
  };

  // 状态
  const storedValue = ref(readValue());

  // 写入localStorage
  const setValue = (value) => {
    try {
      // 允许值是函数,像useState一样
      const valueToStore =
        typeof value === 'function' ? value(storedValue.value) : value;

      // 保存状态
      storedValue.value = valueToStore;

      // 保存到localStorage
      window.localStorage.setItem(key, JSON.stringify(valueToStore));

      // 触发storage事件,让其他标签页也能响应
      window.dispatchEvent(new StorageEvent('storage', { key }));
    } catch (error) {
      console.warn(`写入localStorage键"${key}"时出错:`, error);
    }
  };

  // 移除
  const removeValue = () => {
    try {
      window.localStorage.removeItem(key);
      storedValue.value = initialValue;
      window.dispatchEvent(new StorageEvent('storage', { key }));
    } catch (error) {
      console.warn(`移除localStorage键"${key}"时出错:`, error);
    }
  };

  // 监听其他标签页的变化
  const handleStorageChange = (event) => {
    if (event.key === key && event.storageArea === window.localStorage) {
      storedValue.value = readValue();
    }
  };

  // 添加事件监听
  window.addEventListener('storage', handleStorageChange);

  // 组件卸载时清理
  const cleanup = () => {
    window.removeEventListener('storage', handleStorageChange);
  };

  return {
    value: storedValue,
    setValue,
    removeValue,
    cleanup
  };
}

// useSessionStorage.js - SessionStorage版本
export function useSessionStorage(key, initialValue) {
  const readValue = () => {
    try {
      const item = window.sessionStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`读取sessionStorage键"${key}"时出错:`, error);
      return initialValue;
    }
  };

  const storedValue = ref(readValue());

  const setValue = (value) => {
    try {
      const valueToStore =
        typeof value === 'function' ? value(storedValue.value) : value;

      storedValue.value = valueToStore;
      window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`写入sessionStorage键"${key}"时出错:`, error);
    }
  };

  const removeValue = () => {
    try {
      window.sessionStorage.removeItem(key);
      storedValue.value = initialValue;
    } catch (error) {
      console.warn(`移除sessionStorage键"${key}"时出错:`, error);
    }
  };

  return {
    value: storedValue,
    setValue,
    removeValue
  };
}

// 在组件中使用
<script setup>
import { useLocalStorage, useSessionStorage } from './useStorage';

// 使用localStorage
const { value: theme, setValue: setTheme } = useLocalStorage('theme', 'light');

const { value: userPreferences } = useLocalStorage('userPreferences', {
  notifications: true,
  language: 'zh-CN',
  fontSize: 14
});

// 使用sessionStorage
const { value: sessionToken, setValue: setSessionToken } =
  useSessionStorage('sessionToken', null);

// 方法
const toggleTheme = () => {
  setTheme(theme.value === 'light' ? 'dark' : 'light');
};

const updatePreferences = () => {
  setTheme({
    ...userPreferences.value,
    fontSize: 16,
    theme: theme.value
  });
};

const login = () => {
  setSessionToken('mock-jwt-token-123456');
};

const logout = () => {
  setSessionToken(null);
};
</script>

<template>
  <div :class="theme">
    <h3>本地存储示例</h3>

    <p>当前主题: {{ theme }}</p>
    <button @click="toggleTheme">切换主题</button>

    <div style="margin-top: 20px;">
      <p>用户偏好设置:</p>
      <pre>{{ userPreferences }}</pre>
      <button @click="updatePreferences">更新偏好设置</button>
    </div>

    <div style="margin-top: 20px;">
      <p>会话令牌: {{ sessionToken || '未登录' }}</p>
      <button @click="login">登录</button>
      <button @click="logout">登出</button>
    </div>

    <p style="margin-top: 20px; font-size: 12px; color: #666;">
      提示: 尝试在多个标签页中打开此页面,切换主题时会同步更新。
    </p>
  </div>
</template>

<style>
.light {
  background-color: white;
  color: black;
  padding: 20px;
}

.dark {
  background-color: #333;
  color: white;
  padding: 20px;
}
</style>
                                        
组合式函数命名约定: 组合式函数通常以 "use" 开头,如 useMouseuseFetchuseLocalStorage。这有助于区分普通函数和组合式函数,并使代码更易理解。

10. 与 Options API 对比

Composition API vs Options API
对比分析
特性 Options API Composition API
代码组织 按选项类型组织(data、methods、computed等) 按逻辑关注点组织,相关代码在一起
代码复用 Mixin、高阶组件、渲染函数 组合式函数,更灵活和类型安全
TypeScript 支持 支持,但类型推断有限 一流的 TypeScript 支持,更好的类型推断
学习曲线 相对平缓,概念较少 较陡峭,需要理解更多概念
灵活性 固定结构,灵活性较低 非常灵活,可以按需组织代码
打包体积 Tree-shaking 不友好 Tree-shaking 友好,只导入需要的函数
逻辑提取 提取困难,容易产生命名冲突 易于提取为组合式函数
可读性(简单组件) 非常好,结构清晰 一般,需要适应新语法
可读性(复杂组件) 较差,逻辑分散在各处 非常好,逻辑集中

如何选择?

使用 Options API 的场景
  • 简单组件: 逻辑不多的展示组件
  • 原型开发: 快速验证想法
  • 初学者: 更容易理解和上手
  • 遗留项目: 保持一致性
  • 团队偏好: 团队更熟悉 Options API
使用 Composition API 的场景
  • 复杂组件: 大量逻辑需要组织
  • 大型应用: 需要更好的代码复用
  • TypeScript 项目: 需要更好的类型支持
  • 团队协作: 需要清晰的逻辑边界
  • 性能敏感: 需要 Tree-shaking 优化
  • 新项目: 可以使用最新特性
同一功能的不同实现
对比示例
Options API 实现

// 计数器 + 用户信息
export default {
  data() {
    return {
      count: 0,
      user: {
        name: '张三',
        age: 25
      }
    };
  },

  computed: {
    doubledCount() {
      return this.count * 2;
    },
    userName() {
      return this.user.name;
    },
    canVote() {
      return this.user.age >= 18;
    }
  },

  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    updateUser(newUser) {
      this.user = { ...this.user, ...newUser };
    }
  },

  watch: {
    'user.age'(newAge, oldAge) {
      console.log(`年龄从${oldAge}变为${newAge}`);
    },
    count(newCount) {
      console.log(`计数变为${newCount}`);
    }
  },

  mounted() {
    console.log('组件已挂载');
    this.fetchUser();
  },

  methods: {
    fetchUser() {
      // 获取用户数据
    }
  }
};
                                        
Composition API 实现

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

export default {
  setup() {
    // ========== 计数器逻辑 ==========
    const count = ref(0);

    const doubledCount = computed(() => count.value * 2);

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

    const decrement = () => {
      count.value--;
    };

    watch(count, (newCount) => {
      console.log(`计数变为${newCount}`);
    });

    // ========== 用户逻辑 ==========
    const user = reactive({
      name: '张三',
      age: 25
    });

    const userName = computed(() => user.name);
    const canVote = computed(() => user.age >= 18);

    const updateUser = (newUser) => {
      Object.assign(user, newUser);
    };

    watch(
      () => user.age,
      (newAge, oldAge) => {
        console.log(`年龄从${oldAge}变为${newAge}`);
      }
    );

    const fetchUser = async () => {
      // 获取用户数据
    };

    // ========== 生命周期 ==========
    onMounted(() => {
      console.log('组件已挂载');
      fetchUser();
    });

    return {
      // 计数器
      count,
      doubledCount,
      increment,
      decrement,

      // 用户
      user,
      userName,
      canVote,
      updateUser
    };
  }
};
                                        

Composition API 的优势:

  • 逻辑清晰: 计数器逻辑和用户逻辑分别组织在一起
  • 易于提取: 可以将计数器逻辑提取为 useCounter(),用户逻辑提取为 useUser()
  • 类型安全: 更好的 TypeScript 支持
  • 可维护性: 当逻辑变得更复杂时,更容易维护
迁移建议: 新项目建议使用 Composition API。对于现有项目,可以在新组件中使用 Composition API,逐步迁移。两种 API 可以共存,Vue 3 完全支持 Options API。

总结

Composition API 是 Vue 3 最重要的创新之一,它为 Vue 应用开发带来了革命性的改进:

  • 更好的逻辑组织: 相关代码组织在一起,而不是分散在不同的选项中
  • 更好的代码复用: 通过组合式函数实现灵活的逻辑复用
  • 更好的类型支持: 一流的 TypeScript 支持
  • 更小的打包体积: Tree-shaking 友好,只导入需要的函数
  • 更好的可维护性: 特别适合复杂组件和大型应用

虽然 Composition API 的学习曲线比 Options API 更陡峭,但一旦掌握,它将极大地提升你的开发效率和代码质量。对于新项目和复杂应用,Composition API 是更好的选择。