各位观众老爷,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 的 Composition API,这玩意儿可是 Vue 2 Options API 的救星,专门解决逻辑复用和代码组织那些破事儿的。
咱们先回忆回忆 Vue 2 的 Options API,写起来是挺顺手的,data、methods、computed、watch 一字排开,井井有条,像个模范生。但是,一旦项目大了,组件逻辑复杂了,这玩意儿就变成了一团乱麻,各种功能的代码散落在不同的 options 里,想找个东西得翻箱倒柜,想复用一段逻辑更是难上加难。
Options API 的痛苦:代码分散与复用困难
先看个简单的例子,假设我们需要做一个带搜索功能的组件,搜索结果要分页显示:
<template>
<div>
<input type="text" v-model="searchText" @input="searchData">
<ul>
<li v-for="item in displayedData" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</template>
<script>
export default {
data() {
return {
searchText: '',
allData: [], // 假设从服务器获取的数据
currentPage: 1,
pageSize: 10,
};
},
computed: {
filteredData() {
return this.allData.filter(item => item.name.includes(this.searchText));
},
totalPages() {
return Math.ceil(this.filteredData.length / this.pageSize);
},
displayedData() {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
return this.filteredData.slice(startIndex, endIndex);
},
},
methods: {
async searchData() {
// 模拟从服务器获取数据
await new Promise(resolve => setTimeout(resolve, 500));
this.allData = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
{ id: 4, name: 'Apple Pie' },
{ id: 5, name: 'Banana Bread' },
{ id: 6, name: 'Orange Juice' },
{ id: 7, name: 'Grape' },
{ id: 8, name: 'Grapefruit' },
{ id: 9, name: 'Lemon' },
{ id: 10, name: 'Lime' },
{ id: 11, name: 'Mango' },
{ id: 12, name: 'Watermelon' },
];
},
prevPage() {
this.currentPage--;
},
nextPage() {
this.currentPage++;
},
},
};
</script>
看起来还行?那是因为这个组件很简单。 如果再加上排序、筛选、联动选择等等功能,代码就会变得更加臃肿,相关的逻辑分散在 data
、computed
和 methods
中,维护起来简直要命。
更可怕的是,如果另一个组件也需要分页功能,你想把这段分页逻辑复用一下,你会发现,根本没法直接拿来用! 你要么复制粘贴,要么写个 mixin,但 mixin 有命名冲突的风险,而且来源不明,用起来心里没底。
Composition API 的救赎:逻辑聚合与复用
Composition API 的出现,就是为了解决这个问题。 它的核心思想是:把相关的逻辑组织在一起,形成一个个可复用的函数,然后在组件的 setup
函数中调用这些函数,把它们返回给模板使用。
咱们用 Composition API 重写上面的搜索分页组件:
<template>
<div>
<input type="text" v-model="searchText" @input="searchData">
<ul>
<li v-for="item in displayedData" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
// 封装搜索逻辑
function useSearch(allData) {
const searchText = ref('');
const filteredData = computed(() => {
return allData.value.filter(item => item.name.includes(searchText.value));
});
return {
searchText,
filteredData,
};
}
// 封装分页逻辑
function usePagination(data, pageSize) {
const currentPage = ref(1);
const totalPages = computed(() => Math.ceil(data.value.length / pageSize));
const displayedData = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize;
const endIndex = startIndex + pageSize;
return data.value.slice(startIndex, endIndex);
});
const prevPage = () => {
currentPage.value--;
};
const nextPage = () => {
currentPage.value++;
};
return {
currentPage,
totalPages,
displayedData,
prevPage,
nextPage,
};
}
export default {
setup() {
const allData = ref([]);
const { searchText, filteredData } = useSearch(allData);
const { currentPage, totalPages, displayedData, prevPage, nextPage } = usePagination(filteredData, 10);
const searchData = async () => {
// 模拟从服务器获取数据
await new Promise(resolve => setTimeout(resolve, 500));
allData.value = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
{ id: 4, name: 'Apple Pie' },
{ id: 5, name: 'Banana Bread' },
{ id: 6, name: 'Orange Juice' },
{ id: 7, name: 'Grape' },
{ id: 8, name: 'Grapefruit' },
{ id: 9, name: 'Lemon' },
{ id: 10, name: 'Lime' },
{ id: 11, name: 'Mango' },
{ id: 12, name: 'Watermelon' },
];
};
onMounted(searchData); // 组件挂载后立即搜索数据
return {
searchText,
displayedData,
currentPage,
totalPages,
prevPage,
nextPage,
searchData,
};
},
};
</script>
看到了吗? 我们把搜索逻辑和分页逻辑分别封装成了 useSearch
和 usePagination
两个函数。 这样,如果其他组件也需要分页功能,直接 import { usePagination } from './usePagination'
就行了,省时省力。
Composition API 的优势总结
- 更好的代码组织: 相关的逻辑组织在一起,代码可读性更高,更容易维护。
- 更强的逻辑复用: 可以把逻辑封装成可复用的函数(Composables),在多个组件中共享。
- 更灵活的类型推导: Typescript 支持更好,可以更好地利用 Typescript 的类型检查功能。
- 更小的 bundle 体积: Tree-shaking 优化更好,可以减少打包后的代码体积。
深入理解 Composition API 的核心概念
Composition API 涉及几个核心概念,咱们一个个来看:
-
setup()
函数: 这是 Composition API 的入口,所有的逻辑都在这里编写。它接收两个参数:props
: 组件的 props 对象。context
: 组件的上下文对象,包含attrs
、emit
和slots
。
setup()
函数必须返回一个对象,这个对象中的属性和方法才能在模板中使用。 -
ref()
和reactive()
: 这两个函数用于创建响应式数据。ref()
: 用于创建单个值的响应式数据,例如ref(0)
、ref('hello')
。 通过.value
访问和修改 ref 的值。reactive()
: 用于创建对象或数组的响应式数据,例如reactive({ count: 0 })
、reactive([1, 2, 3])
。 直接访问和修改 reactive 对象的属性。
import { ref, reactive } from 'vue'; export default { setup() { const count = ref(0); const state = reactive({ name: 'Vue', version: 3, }); const increment = () => { count.value++; state.version++; }; return { count, state, increment, }; }, };
-
computed()
: 用于创建计算属性,它的值会根据依赖的响应式数据自动更新。import { ref, computed } from 'vue'; export default { setup() { const price = ref(10); const quantity = ref(2); const total = computed(() => price.value * quantity.value); return { price, quantity, total, }; }, };
-
watch()
和watchEffect()
: 用于监听响应式数据的变化。watch()
: 监听指定的响应式数据,当数据发生变化时,执行回调函数。 可以精确地控制监听哪些数据。watchEffect()
: 立即执行一次回调函数,并在回调函数中自动收集依赖的响应式数据。 当依赖的响应式数据发生变化时,再次执行回调函数。 更方便,但是依赖关系可能不明确。
import { ref, watch, watchEffect } from 'vue'; export default { setup() { const count = ref(0); const name = ref('Vue'); // watch watch(count, (newValue, oldValue) => { console.log(`count changed from ${oldValue} to ${newValue}`); }); // watch 多个数据源 watch([count, name], ([newCount, newName], [oldCount, oldName]) => { console.log(`count changed from ${oldCount} to ${newCount}, name changed from ${oldName} to ${newName}`); }); // watchEffect watchEffect(() => { console.log(`count is ${count.value}, name is ${name.value}`); }); const increment = () => { count.value++; }; const changeName = () => { name.value = 'React'; }; return { count, name, increment, changeName, }; }, };
-
生命周期钩子: Composition API 提供了与 Options API 对应的生命周期钩子函数,例如
onMounted
、onUpdated
、onUnmounted
等。import { onMounted, onUpdated, onUnmounted } from 'vue'; export default { setup() { onMounted(() => { console.log('Component mounted'); }); onUpdated(() => { console.log('Component updated'); }); onUnmounted(() => { console.log('Component unmounted'); }); return {}; }, };
-
Provide / Inject: 用于跨组件传递数据,类似于 Options API 的
provide
和inject
。 但是用法上更加灵活,可以配合Symbol
使用,避免命名冲突。// Parent Component import { provide } from 'vue'; const mySymbol = Symbol('mySymbol'); export default { setup() { provide(mySymbol, 'Hello from parent'); return {}; }, }; // Child Component import { inject } from 'vue'; export default { setup() { const message = inject(mySymbol); console.log(message); // Output: Hello from parent return { message, }; }, };
Composables:代码复用的利器
Composables 就是把组件逻辑封装成可复用的函数。 它们通常以 use
开头,例如 useMouse
、useFetch
、useLocalStorage
等。
咱们来创建一个 useMouse
composable,用于追踪鼠标的位置:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (event) => {
x.value = event.pageX;
y.value = event.pageY;
};
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return {
x,
y,
};
}
然后在组件中使用它:
<template>
<div>
Mouse position: x = {{ x }}, y = {{ y }}
</div>
</template>
<script>
import { useMouse } from './useMouse';
export default {
setup() {
const { x, y } = useMouse();
return {
x,
y,
};
},
};
</script>
这样,任何组件都可以通过 useMouse
来追踪鼠标的位置,大大提高了代码的复用性。
Options API 与 Composition API 的对比
为了更清晰地理解 Composition API 的优势,咱们用表格对比一下 Options API 和 Composition API:
特性 | Options API | Composition API |
---|---|---|
代码组织 | 基于 Options (data, methods, computed, watch) | 基于逻辑功能 (Composables) |
逻辑复用 | Mixins (易冲突,来源不明) | Composables (清晰,可测试) |
类型推导 | 较弱 | 更强 (Typescript 支持更好) |
可读性 | 小组件易读,大组件混乱 | 大小组件都易读,逻辑清晰 |
Bundle 大小 | 较大 | 较小 (Tree-shaking 优化更好) |
学习曲线 | 简单易上手 | 稍难,需要理解 ref, reactive 等概念,但长期来看更高效 |
最佳实践与注意事项
- 合理拆分 Composables: 不要把所有的逻辑都塞到一个 Composables 里面,尽量保持 Composables 的单一职责。
- 命名规范: Composables 应该以
use
开头,例如useFetch
、useLocalStorage
。 - 类型安全: 尽量使用 Typescript 来编写 Composables,提高代码的健壮性。
- 不要过度使用
watchEffect
: 尽量使用watch
来精确地监听需要监听的数据,避免不必要的性能开销。 - 与 Options API 混合使用: 在 Vue 3 中,可以同时使用 Options API 和 Composition API。 可以逐步将 Options API 的代码迁移到 Composition API。
总结
Composition API 是 Vue 3 中一项强大的特性,它解决了 Options API 在大型项目中代码组织和逻辑复用方面的痛点。 通过将相关的逻辑组织成可复用的 Composables,Composition API 提高了代码的可读性、可维护性和可测试性。 虽然学习曲线稍陡峭,但长期来看,Composition API 能够显著提高开发效率和代码质量。
好了,今天的讲座就到这里,希望大家有所收获! 记住,技术是为我们服务的,选择最适合自己的方案才是最重要的! 感谢大家的观看,下次再见!