Options API 与 Composition API 对照表

2 阅读7分钟

Options API 与 Composition API 对照表

学习目标

完成本章学习后,你将能够:

  • 理解Options API和Composition API的核心区别
  • 快速将Options API代码转换为Composition API
  • 根据项目需求选择合适的API风格
  • 理解两种API风格的优缺点和适用场景

前置知识

学习本章内容前,你需要掌握:

问题引入

实际场景

小李是一名前端开发者,公司的老项目使用Vue 2和Options API编写。现在公司决定将项目升级到Vue 3,并逐步迁移到Composition API。小李需要:

  1. 理解两种API的对应关系:如何将Options API的data、methods、computed等转换为Composition API?
  2. 保持功能一致性:迁移后的代码需要保持原有功能不变
  3. 利用新特性:在迁移过程中,如何利用Composition API的优势改进代码?
  4. 团队协作:如何让团队成员快速理解两种API的区别?

为什么需要这个对照表

Options API和Composition API是Vue提供的两种不同的组件编写方式:

  • Options API:Vue 2的传统写法,通过配置对象(data、methods、computed等)组织代码
  • Composition API:Vue 3引入的新写法,通过组合函数的方式组织代码,提供更好的逻辑复用和类型推导

这个对照表将帮助你:

  1. 快速找到Options API在Composition API中的对应写法
  2. 理解两种API风格的设计思想差异
  3. 顺利完成项目迁移和代码重构
  4. 在新项目中做出合适的技术选择

核心概念

概念1:API风格对比

Options API特点

Options API通过配置对象的方式组织代码,每个选项负责特定的功能:

export default {
  data() {
    return {
      // 响应式数据
    }
  },
  computed: {
    // 计算属性
  },
  methods: {
    // 方法
  },
  mounted() {
    // 生命周期钩子
  }
}

优点

  • 结构清晰,容易理解
  • 适合小型组件
  • 学习曲线平缓

缺点

  • 逻辑分散在不同选项中
  • 难以复用逻辑
  • TypeScript支持较弱
Composition API特点

Composition API通过组合函数的方式组织代码,相关逻辑可以放在一起:

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

// 所有逻辑都在setup中,可以按功能组织
const count = ref(0);
const doubled = computed(() => count.value * 2);

onMounted(() => {
  // 生命周期逻辑
});
</script>

优点

  • 逻辑复用更容易(组合式函数)
  • 更好的TypeScript支持
  • 更灵活的代码组织
  • 更好的tree-shaking

缺点

  • 学习曲线较陡
  • 需要理解ref和reactive的区别
  • 代码可能不如Options API结构化

概念2:核心API对照

下面是两种API风格的完整对照表:

完整对照表

1. 响应式数据

Options APIComposition API说明
data()ref() / reactive()定义响应式数据

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0,
      user: {
        name: '张三',
        age: 25
      }
    }
  }
}
</script>

Composition API示例:

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

// 使用ref定义基本类型
const count = ref(0);

// 使用reactive定义对象
const user = reactive({
  name: '张三',
  age: 25
});

// 注意:访问ref需要.value,reactive不需要
console.log(count.value); // 0
console.log(user.name);   // '张三'
</script>

2. 计算属性

Options APIComposition API说明
computedcomputed()定义计算属性

Options API示例:

<script>
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    // 只读计算属性
    fullName() {
      return this.firstName + this.lastName;
    },
    // 可写计算属性
    reversedName: {
      get() {
        return this.lastName + this.firstName;
      },
      set(value) {
        this.lastName = value[0];
        this.firstName = value.slice(1);
      }
    }
  }
}
</script>

Composition API示例:

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

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

// 只读计算属性
const fullName = computed(() => {
  return firstName.value + lastName.value;
});

// 可写计算属性
const reversedName = computed({
  get() {
    return lastName.value + firstName.value;
  },
  set(value) {
    lastName.value = value[0];
    firstName.value = value.slice(1);
  }
});
</script>

3. 方法

Options APIComposition API说明
methods普通函数定义方法

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    reset() {
      this.count = 0;
    }
  }
}
</script>

Composition API示例:

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

const count = ref(0);

// 直接定义函数,不需要methods选项
const increment = () => {
  count.value++;
};

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

const reset = () => {
  count.value = 0;
};
</script>

4. 侦听器

Options APIComposition API说明
watchwatch() / watchEffect()侦听数据变化

Options API示例:

<script>
export default {
  data() {
    return {
      question: '',
      answer: '请输入问题'
    }
  },
  watch: {
    // 简单侦听
    question(newValue, oldValue) {
      console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
      this.getAnswer();
    },
    // 深度侦听
    user: {
      handler(newValue, oldValue) {
        console.log('用户信息变化');
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    getAnswer() {
      this.answer = '正在思考...';
    }
  }
}
</script>

Composition API示例:

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

const question = ref('');
const answer = ref('请输入问题');
const user = ref({ name: '张三', age: 25 });

// 使用watch侦听特定数据源
watch(question, (newValue, oldValue) => {
  console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
  getAnswer();
});

// 深度侦听对象
watch(user, (newValue, oldValue) => {
  console.log('用户信息变化');
}, {
  deep: true,
  immediate: true
});

// 使用watchEffect自动追踪依赖
watchEffect(() => {
  // 自动追踪question的变化
  console.log(`当前问题:${question.value}`);
});

const getAnswer = () => {
  answer.value = '正在思考...';
};
</script>

5. 生命周期钩子

Options APIComposition API说明
beforeCreate-使用setup()替代
created-使用setup()替代
beforeMountonBeforeMount()挂载前
mountedonMounted()挂载后
beforeUpdateonBeforeUpdate()更新前
updatedonUpdated()更新后
beforeUnmountonBeforeUnmount()卸载前
unmountedonUnmounted()卸载后
errorCapturedonErrorCaptured()错误捕获
activatedonActivated()keep-alive激活
deactivatedonDeactivated()keep-alive停用

Options API示例:

<script>
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeCreate() {
    console.log('beforeCreate: 实例初始化之后');
  },
  created() {
    console.log('created: 实例创建完成');
    // 可以访问this.message
  },
  beforeMount() {
    console.log('beforeMount: 挂载开始之前');
  },
  mounted() {
    console.log('mounted: 挂载完成');
    // 可以访问DOM
  },
  beforeUpdate() {
    console.log('beforeUpdate: 数据更新前');
  },
  updated() {
    console.log('updated: 数据更新后');
  },
  beforeUnmount() {
    console.log('beforeUnmount: 卸载前');
  },
  unmounted() {
    console.log('unmounted: 卸载完成');
    // 清理定时器、事件监听等
  }
}
</script>

Composition API示例:

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

const message = ref('Hello');

// setup()本身就相当于beforeCreate和created
console.log('setup执行,相当于created');

onBeforeMount(() => {
  console.log('onBeforeMount: 挂载开始之前');
});

onMounted(() => {
  console.log('onMounted: 挂载完成');
  // 可以访问DOM
});

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

onUpdated(() => {
  console.log('onUpdated: 数据更新后');
});

onBeforeUnmount(() => {
  console.log('onBeforeUnmount: 卸载前');
});

onUnmounted(() => {
  console.log('onUnmounted: 卸载完成');
  // 清理定时器、事件监听等
});
</script>

6. Props

Options APIComposition API说明
propsdefineProps()定义组件属性

Options API示例:

<script>
export default {
  props: {
    // 简单声明
    title: String,
    // 详细声明
    count: {
      type: Number,
      required: true,
      default: 0,
      validator(value) {
        return value >= 0;
      }
    },
    user: {
      type: Object,
      default: () => ({ name: '匿名' })
    }
  },
  mounted() {
    // 通过this访问props
    console.log(this.title);
    console.log(this.count);
  }
}
</script>

Composition API示例:

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

// 简单声明
// const props = defineProps(['title', 'count']);

// 详细声明(推荐)
const props = defineProps({
  title: String,
  count: {
    type: Number,
    required: true,
    default: 0,
    validator(value) {
      return value >= 0;
    }
  },
  user: {
    type: Object,
    default: () => ({ name: '匿名' })
  }
});

// TypeScript类型声明(更推荐)
// const props = defineProps<{
//   title?: string;
//   count: number;
//   user?: { name: string };
// }>();

// 直接访问props,不需要this
console.log(props.title);
console.log(props.count);

// props是响应式的,可以在computed中使用
const doubledCount = computed(() => props.count * 2);
</script>

7. Emits(事件)

Options APIComposition API说明
emits + $emitdefineEmits()定义和触发事件

Options API示例:

<script>
export default {
  emits: ['update', 'delete'],
  // 或者详细声明
  emits: {
    update: (value) => {
      // 验证事件参数
      return typeof value === 'string';
    },
    delete: null
  },
  methods: {
    handleClick() {
      // 触发事件
      this.$emit('update', 'new value');
    },
    handleDelete() {
      this.$emit('delete');
    }
  }
}
</script>

Composition API示例:

<script setup>
// 简单声明
// const emit = defineEmits(['update', 'delete']);

// 详细声明(推荐)
const emit = defineEmits({
  update: (value) => {
    // 验证事件参数
    return typeof value === 'string';
  },
  delete: null
});

// TypeScript类型声明(更推荐)
// const emit = defineEmits<{
//   update: [value: string];
//   delete: [];
// }>();

const handleClick = () => {
  // 触发事件
  emit('update', 'new value');
};

const handleDelete = () => {
  emit('delete');
};
</script>

8. 插槽

Options APIComposition API说明
$slotsuseSlots()访问插槽

Options API示例:

<script>
export default {
  mounted() {
    // 检查插槽是否存在
    if (this.$slots.default) {
      console.log('有默认插槽内容');
    }
    if (this.$slots.header) {
      console.log('有header插槽内容');
    }
  }
}
</script>

<template>
  <div>
    <header v-if="$slots.header">
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
  </div>
</template>

Composition API示例:

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

// 获取插槽对象
const slots = useSlots();

onMounted(() => {
  // 检查插槽是否存在
  if (slots.default) {
    console.log('有默认插槽内容');
  }
  if (slots.header) {
    console.log('有header插槽内容');
  }
});
</script>

<template>
  <div>
    <header v-if="slots.header">
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
  </div>
</template>

9. Refs(模板引用)

Options APIComposition API说明
$refsref()访问DOM或组件实例

Options API示例:

<script>
export default {
  mounted() {
    // 访问DOM元素
    console.log(this.$refs.input);
    this.$refs.input.focus();
    
    // 访问子组件实例
    console.log(this.$refs.child);
    this.$refs.child.someMethod();
  }
}
</script>

<template>
  <div>
    <input ref="input" />
    <ChildComponent ref="child" />
  </div>
</template>

Composition API示例:

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

// 创建ref,变量名必须与模板中的ref属性值相同
const input = ref(null);
const child = ref(null);

onMounted(() => {
  // 访问DOM元素
  console.log(input.value);
  input.value.focus();
  
  // 访问子组件实例
  console.log(child.value);
  child.value.someMethod();
});
</script>

<template>
  <div>
    <input ref="input" />
    <ChildComponent ref="child" />
  </div>
</template>

10. 暴露组件方法

Options APIComposition API说明
自动暴露defineExpose()暴露组件内部方法给父组件

Options API示例:

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    getCount() {
      return this.count;
    }
  }
  // Options API中,所有methods和data都会自动暴露给父组件
}
</script>

Composition API示例:

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

const count = ref(0);

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

const getCount = () => {
  return count.value;
};

// Composition API中,默认不暴露任何内容
// 需要使用defineExpose显式暴露
defineExpose({
  increment,
  getCount
  // 注意:通常不暴露响应式数据本身
});
</script>

11. Provide / Inject(依赖注入)

Options APIComposition API说明
provide / injectprovide() / inject()跨层级组件通信

Options API示例:

<!-- 祖先组件 -->
<script>
export default {
  data() {
    return {
      theme: 'dark'
    }
  },
  provide() {
    return {
      theme: this.theme,
      // 注意:这样提供的值不是响应式的
      updateTheme: this.updateTheme
    }
  },
  methods: {
    updateTheme(newTheme) {
      this.theme = newTheme;
    }
  }
}
</script>

<!-- 后代组件 -->
<script>
export default {
  inject: ['theme', 'updateTheme'],
  mounted() {
    console.log(this.theme); // 'dark'
    this.updateTheme('light');
  }
}
</script>

Composition API示例:

<!-- 祖先组件 -->
<script setup>
import { ref, provide } from 'vue';

const theme = ref('dark');

const updateTheme = (newTheme) => {
  theme.value = newTheme;
};

// 提供响应式数据
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>

<!-- 后代组件 -->
<script setup>
import { inject, onMounted } from 'vue';

// 注入数据,可以提供默认值
const theme = inject('theme', 'light');
const updateTheme = inject('updateTheme');

onMounted(() => {
  console.log(theme.value); // 'dark'
  updateTheme('light');
});
</script>

12. Mixins(混入)

Options APIComposition API说明
mixins组合式函数逻辑复用

Options API示例:

// mixins/logger.js
export const loggerMixin = {
  data() {
    return {
      logCount: 0
    }
  },
  methods: {
    log(message) {
      console.log(message);
      this.logCount++;
    }
  },
  mounted() {
    console.log('Logger mixin mounted');
  }
};

// 使用mixin
export default {
  mixins: [loggerMixin],
  mounted() {
    this.log('组件已挂载');
    console.log(`日志次数:${this.logCount}`);
  }
}

Composition API示例:

// composables/useLogger.js
import { ref, onMounted } from 'vue';

export function useLogger() {
  const logCount = ref(0);
  
  const log = (message) => {
    console.log(message);
    logCount.value++;
  };
  
  onMounted(() => {
    console.log('Logger composable mounted');
  });
  
  return {
    logCount,
    log
  };
}

// 使用组合式函数
import { useLogger } from './composables/useLogger';

const { logCount, log } = useLogger();

onMounted(() => {
  log('组件已挂载');
  console.log(`日志次数:${logCount.value}`);
});

最佳实践

企业级应用场景

场景1:大型组件迁移策略

在企业级项目中,通常不会一次性将所有组件从Options API迁移到Composition API。推荐采用渐进式迁移策略:

<!-- 步骤1:保持Options API,先熟悉Composition API -->
<script>
export default {
  // 保持原有Options API代码
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<!-- 步骤2:混合使用,逐步迁移 -->
<script>
import { ref, computed } from 'vue';

export default {
  // 新功能使用Composition API
  setup() {
    const newFeature = ref('');
    const processedFeature = computed(() => newFeature.value.toUpperCase());
    
    return {
      newFeature,
      processedFeature
    };
  },
  // 旧功能保持Options API
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<!-- 步骤3:完全迁移到Composition API -->
<script setup>
import { ref, computed } from 'vue';

// 所有逻辑都使用Composition API
const count = ref(0);
const message = ref('Hello');
const newFeature = ref('');

const processedFeature = computed(() => newFeature.value.toUpperCase());

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

迁移建议

  1. 从小型、独立的组件开始迁移
  2. 优先迁移新功能和新组件
  3. 对于复杂组件,可以先混合使用
  4. 充分测试迁移后的功能
  5. 团队成员需要先学习Composition API基础
场景2:逻辑复用最佳实践

Composition API的最大优势是逻辑复用。下面是一个完整的企业级示例:

// composables/useUserManagement.js
/**
 * 用户管理组合式函数
 * 封装用户相关的所有逻辑,包括获取、更新、删除等操作
 */
import { ref, computed, onMounted } from 'vue';
import { userApi } from '@/api/user';

export function useUserManagement() {
  // 响应式状态
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);
  const currentPage = ref(1);
  const pageSize = ref(10);
  
  // 计算属性
  const totalPages = computed(() => {
    return Math.ceil(users.value.length / pageSize.value);
  });
  
  const paginatedUsers = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value;
    const end = start + pageSize.value;
    return users.value.slice(start, end);
  });
  
  // 方法
  const fetchUsers = async () => {
    loading.value = true;
    error.value = null;
    try {
      const response = await userApi.getUsers();
      users.value = response.data;
    } catch (err) {
      error.value = err.message;
      console.error('获取用户列表失败:', err);
    } finally {
      loading.value = false;
    }
  };
  
  const deleteUser = async (userId) => {
    try {
      await userApi.deleteUser(userId);
      // 从列表中移除已删除的用户
      users.value = users.value.filter(user => user.id !== userId);
    } catch (err) {
      error.value = err.message;
      throw err;
    }
  };
  
  const updateUser = async (userId, userData) => {
    try {
      const response = await userApi.updateUser(userId, userData);
      // 更新列表中的用户数据
      const index = users.value.findIndex(user => user.id === userId);
      if (index !== -1) {
        users.value[index] = response.data;
      }
    } catch (err) {
      error.value = err.message;
      throw err;
    }
  };
  
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page;
    }
  };
  
  // 生命周期
  onMounted(() => {
    fetchUsers();
  });
  
  // 返回需要暴露的状态和方法
  return {
    // 状态
    users,
    loading,
    error,
    currentPage,
    pageSize,
    // 计算属性
    totalPages,
    paginatedUsers,
    // 方法
    fetchUsers,
    deleteUser,
    updateUser,
    goToPage
  };
}

在组件中使用:

<script setup>
import { useUserManagement } from '@/composables/useUserManagement';
import { ElMessage } from 'element-plus';

// 使用组合式函数,获取所有用户管理相关的功能
const {
  paginatedUsers,
  loading,
  error,
  currentPage,
  totalPages,
  deleteUser,
  goToPage
} = useUserManagement();

// 处理删除操作
const handleDelete = async (userId) => {
  try {
    await deleteUser(userId);
    ElMessage.success('删除成功');
  } catch (err) {
    ElMessage.error('删除失败');
  }
};
</script>

<template>
  <div class="user-management">
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误提示 -->
    <div v-if="error" class="error">{{ error }}</div>
    
    <!-- 用户列表 -->
    <div v-else class="user-list">
      <div v-for="user in paginatedUsers" :key="user.id" class="user-item">
        <span>{{ user.name }}</span>
        <button @click="handleDelete(user.id)">删除</button>
      </div>
    </div>
    
    <!-- 分页 -->
    <div class="pagination">
      <button 
        @click="goToPage(currentPage - 1)" 
        :disabled="currentPage === 1"
      >
        上一页
      </button>
      <span>{{ currentPage }} / {{ totalPages }}</span>
      <button 
        @click="goToPage(currentPage + 1)" 
        :disabled="currentPage === totalPages"
      >
        下一页
      </button>
    </div>
  </div>
</template>

优势分析

  1. 逻辑集中:所有用户管理相关的逻辑都在一个文件中
  2. 易于复用:多个组件可以使用同一个组合式函数
  3. 易于测试:可以单独测试组合式函数
  4. 类型安全:配合TypeScript可以获得完整的类型提示
  5. 按需引入:只引入需要的功能,减少组件代码量

常见陷阱

陷阱1:忘记.value

错误示例:

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

const count = ref(0);

const increment = () => {
  // ❌ 错误:忘记使用.value
  count++;  // 这不会触发响应式更新
};
</script>

正确做法:

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

const count = ref(0);

const increment = () => {
  // ✅ 正确:使用.value访问和修改ref的值
  count.value++;
};
</script>

原因分析

  • ref()返回的是一个响应式引用对象,不是原始值
  • 在JavaScript中访问和修改ref需要使用.value
  • 在模板中Vue会自动解包,不需要.value
陷阱2:reactive对象的解构

错误示例:

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

const state = reactive({
  count: 0,
  message: 'Hello'
});

// ❌ 错误:直接解构会失去响应性
const { count, message } = state;

const increment = () => {
  count++;  // 这不会触发响应式更新
};
</script>

正确做法:

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

const state = reactive({
  count: 0,
  message: 'Hello'
});

// ✅ 正确:使用toRefs保持响应性
const { count, message } = toRefs(state);

const increment = () => {
  count.value++;  // 现在可以正常工作
};

// 或者不解构,直接使用state
const increment2 = () => {
  state.count++;  // 这也可以正常工作
};
</script>

原因分析

  • 直接解构reactive对象会失去响应性
  • toRefs()reactive对象的每个属性转换为ref
  • 转换后的ref保持与原对象的响应式连接
陷阱3:watch的immediate选项

错误示例:

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

const userId = ref(null);
const userData = ref(null);

// ❌ 问题:只有userId变化时才会执行
watch(userId, async (newId) => {
  if (newId) {
    const response = await fetchUser(newId);
    userData.value = response.data;
  }
});

// 如果userId初始值不是null,watch不会立即执行
// 需要手动调用一次fetchUser
</script>

正确做法:

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

const userId = ref(123);  // 初始值不是null
const userData = ref(null);

// ✅ 正确:使用immediate选项立即执行一次
watch(userId, async (newId) => {
  if (newId) {
    const response = await fetchUser(newId);
    userData.value = response.data;
  }
}, {
  immediate: true  // 组件挂载时立即执行一次
});
</script>

原因分析

  • 默认情况下,watch只在数据变化时执行
  • 使用immediate: true可以在组件挂载时立即执行一次
  • 这对于需要根据初始值加载数据的场景非常有用

性能优化建议

建议1:合理选择ref和reactive
<script setup>
import { ref, reactive } from 'vue';

// ✅ 推荐:基本类型使用ref
const count = ref(0);
const message = ref('Hello');
const isActive = ref(false);

// ✅ 推荐:对象使用reactive(如果不需要整体替换)
const user = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
});

// ❌ 不推荐:对象使用ref(除非需要整体替换)
const user2 = ref({
  name: '李四',
  age: 30
});
// 访问属性需要user2.value.name,比较繁琐

// ✅ 但如果需要整体替换对象,ref更合适
const config = ref({ theme: 'dark' });
// 可以整体替换
config.value = { theme: 'light', fontSize: 14 };
</script>
建议2:使用computed缓存计算结果
<script setup>
import { ref, computed } from 'vue';

const items = ref([
  { id: 1, name: '商品A', price: 100, quantity: 2 },
  { id: 2, name: '商品B', price: 200, quantity: 1 },
  { id: 3, name: '商品C', price: 150, quantity: 3 }
]);

// ✅ 推荐:使用computed缓存计算结果
const totalPrice = computed(() => {
  console.log('计算总价');  // 只在items变化时执行
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
});

// ❌ 不推荐:使用方法每次都重新计算
const getTotalPrice = () => {
  console.log('计算总价');  // 每次调用都执行
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
};
</script>

<template>
  <div>
    <!-- computed会缓存结果,多次使用不会重复计算 -->
    <p>总价:{{ totalPrice }}</p>
    <p>总价(含税):{{ totalPrice * 1.1 }}</p>
    
    <!-- 方法每次都会重新计算 -->
    <p>总价:{{ getTotalPrice() }}</p>
    <p>总价(含税):{{ getTotalPrice() * 1.1 }}</p>
  </div>
</template>
建议3:避免在模板中使用复杂表达式
<script setup>
import { ref, computed } from 'vue';

const users = ref([
  { id: 1, name: '张三', age: 25, status: 'active' },
  { id: 2, name: '李四', age: 30, status: 'inactive' },
  { id: 3, name: '王五', age: 28, status: 'active' }
]);

// ❌ 不推荐:在模板中使用复杂表达式
// <div v-for="user in users.filter(u => u.status === 'active').sort((a, b) => a.age - b.age)">

// ✅ 推荐:使用computed处理复杂逻辑
const activeUsers = computed(() => {
  return users.value
    .filter(user => user.status === 'active')
    .sort((a, b) => a.age - b.age);
});
</script>

<template>
  <div>
    <!-- 模板更简洁,逻辑更清晰 -->
    <div v-for="user in activeUsers" :key="user.id">
      {{ user.name }} - {{ user.age }}岁
    </div>
  </div>
</template>

实践练习

练习1:Options API转Composition API(难度:简单)

需求描述

将下面的Options API组件转换为Composition API:

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  },
  methods: {
    updateFirstName(value) {
      this.firstName = value;
    },
    updateLastName(value) {
      this.lastName = value;
    }
  },
  mounted() {
    console.log('组件已挂载');
  }
}
</script>

<template>
  <div>
    <input :value="firstName" @input="updateFirstName($event.target.value)" />
    <input :value="lastName" @input="updateLastName($event.target.value)" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

实现提示

  1. 使用ref()定义响应式数据
  2. 使用computed()定义计算属性
  3. 直接定义函数替代methods
  4. 使用onMounted()替代mounted钩子
  5. 记得在访问ref时使用.value

参考答案

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

// 使用ref定义响应式数据
const firstName = ref('');
const lastName = ref('');

// 使用computed定义计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

// 直接定义函数
const updateFirstName = (value) => {
  firstName.value = value;
};

const updateLastName = (value) => {
  lastName.value = value;
};

// 使用onMounted替代mounted钩子
onMounted(() => {
  console.log('组件已挂载');
});
</script>

<template>
  <div>
    <!-- 模板部分保持不变 -->
    <input :value="firstName" @input="updateFirstName($event.target.value)" />
    <input :value="lastName" @input="updateLastName($event.target.value)" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

答案解析

  1. 响应式数据:使用ref()替代data(),因为firstName和lastName是基本类型
  2. 计算属性computed()的用法与Options API类似,但需要使用.value访问ref
  3. 方法:直接定义函数,不需要methods选项
  4. 生命周期:使用onMounted()替代mounted()钩子
  5. 模板:模板部分不需要修改,Vue会自动解包ref

练习2:创建可复用的组合式函数(难度:中等)

需求描述

创建一个useCounter组合式函数,实现以下功能:

  1. 维护一个计数器状态
  2. 提供增加、减少、重置方法
  3. 提供一个计算属性显示计数器是否为偶数
  4. 支持设置初始值和步长
  5. 在两个不同的组件中使用这个组合式函数

实现提示

  1. 创建一个独立的文件存放组合式函数
  2. 使用ref定义响应式状态
  3. 使用computed定义计算属性
  4. 函数接受配置参数(初始值、步长)
  5. 返回需要暴露的状态和方法

参考答案

// composables/useCounter.js
/**
 * 计数器组合式函数
 * @param {number} initialValue - 初始值,默认为0
 * @param {number} step - 步长,默认为1
 * @returns {Object} 计数器状态和方法
 */
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0, step = 1) {
  // 响应式状态
  const count = ref(initialValue);
  
  // 计算属性:判断是否为偶数
  const isEven = computed(() => {
    return count.value % 2 === 0;
  });
  
  // 方法:增加
  const increment = () => {
    count.value += step;
  };
  
  // 方法:减少
  const decrement = () => {
    count.value -= step;
  };
  
  // 方法:重置
  const reset = () => {
    count.value = initialValue;
  };
  
  // 方法:设置为指定值
  const setValue = (value) => {
    count.value = value;
  };
  
  // 返回需要暴露的内容
  return {
    count,
    isEven,
    increment,
    decrement,
    reset,
    setValue
  };
}

组件A:基础计数器

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

// 使用默认配置
const { count, isEven, increment, decrement, reset } = useCounter();
</script>

<template>
  <div class="counter">
    <h2>基础计数器</h2>
    <p>当前值:{{ count }}</p>
    <p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
    <div class="buttons">
      <button @click="decrement">-1</button>
      <button @click="reset">重置</button>
      <button @click="increment">+1</button>
    </div>
  </div>
</template>

组件B:自定义步长计数器

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

// 使用自定义配置:初始值100,步长10
const { count, isEven, increment, decrement, reset, setValue } = useCounter(100, 10);

// 可以创建多个独立的计数器实例
const counter2 = useCounter(0, 5);
</script>

<template>
  <div class="counter">
    <h2>自定义步长计数器</h2>
    <p>当前值:{{ count }}</p>
    <p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
    <div class="buttons">
      <button @click="decrement">-10</button>
      <button @click="reset">重置到100</button>
      <button @click="increment">+10</button>
      <button @click="setValue(0)">设置为0</button>
    </div>
    
    <hr />
    
    <h2>第二个计数器(步长5)</h2>
    <p>当前值:{{ counter2.count }}</p>
    <div class="buttons">
      <button @click="counter2.decrement">-5</button>
      <button @click="counter2.increment">+5</button>
    </div>
  </div>
</template>

答案解析

  1. 组合式函数设计

    • 接受配置参数,提供灵活性
    • 封装所有相关逻辑,包括状态、计算属性和方法
    • 返回对象,方便按需解构
  2. 逻辑复用

    • 同一个组合式函数可以在多个组件中使用
    • 每次调用都创建独立的实例,互不影响
    • 可以在同一个组件中创建多个实例
  3. 优势体现

    • 代码复用:避免重复编写相同逻辑
    • 逻辑集中:所有计数器相关逻辑都在一个文件中
    • 易于测试:可以单独测试组合式函数
    • 类型安全:配合TypeScript可以获得完整的类型提示
  4. 与Mixin对比

    • 组合式函数的来源清晰(显式导入)
    • 不会有命名冲突问题
    • 可以传递参数配置
    • 更好的TypeScript支持

进阶阅读