组合式
选项式 API
在 Vue 引入组合式 API 之前,开发者们使用 选项式 API
来组织逻辑。其中包含数据、生命周期方法、计算属性等。选项式 API 允许使用特定的选项来定义这些逻辑,就像下面所展示的这样:
<!-- 模板 省略 -->
<script>
export default {
name: "MyComponent",
props: {
// 属性
},
data() {
// 数据
},
computed: {
// 计算属性
},
watch: {
// 监听属性
},
methods: {
// 方法
},
created() {
// 生命周期函数
},
// ...
};
</script>
<!-- 样式 省略 -->
虽然这种写法可以实现需求,并且在 Vue 3 中依然可用,但随着组件逐渐增大并日益复杂,组件的管理和维护将变得越来越困难。这种使用特定选项来定义组件的方式会让代码变得越来越难以阅读和理解,这种不适的感觉会在处理大量的组件时体验更加明显。这种代码组织方式下,提取和复用组件逻辑也很困难。
我们看一个 App
组件的例子,它渲染了两个子组件:Count
和 Width
。
<template>
<div class="App">
<Count :count="count" :increment="increment" :decrement="decrement" />
<div id="divider" />
<Width :width="width" />
</div>
</template>
<script>
import Count from "./components/Count.vue";
import Width from "./components/Width.vue";
export default {
name: "App",
data() {
return {
count: 0,
width: 0,
};
},
mounted() {
this.handleResize();
window.addEventListener("resize", this.handleResize);
},
beforeUnmount() {
window.removeEventListener("resize", this.handleResize);
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
handleResize() {
this.width = window.innerWidth;
},
},
components: {
Count,
Width,
},
};
</script>
上面的代码就是一个叫作 App
的单文件组件(SFC)。
template
部分定义了组件的元素。在这个例子中,这个组件包含一个带有 App
class 的 div
元素,其中包含两个子组件:<Count>
和 <Width>
。这些子组件都使用了 Vue 的属性绑定语法(:count
、:increment
、:decrement
和 :width
)来传递属性。
<script>
部分包含了组件的 JavaScript 代码。在 JavaScript 的开始,从 Count
和 Width
各自的文件中导入了它们的组件。export default
部分用于导出组件定义。在组件的定义中,我们做了:
data
方法返回一个带有组件属性初始值的对象,属性count
和width
都初始化为0
。mounted()
生命周期钩子用于在组件挂载到 DOM 上之后执行一些代码。在这个例子中,组件在挂载后执行了handleSize()
方法,在resize
事件上添加了一个事件监听。beforeUnmount()
生命周期钩子用于组件卸载销毁之前执行一些代码。在这个例子中,它移除了在resize
事件中的监听。methods
对象包含了组件的方法。这个例子中定义了increment()
、decrement()
和handleResize()
方法, 这些方法用于在某些时机操作count
和width
数据。
当应用运行后,当前的计数和窗口的内部宽度会根据数据变化实时显示。用户可以使用按钮来增加或减少计数,这样来与组件交互。
同样地,在窗口宽度发生变化时,宽度的显示也会自动更新。
单文件组件 App.vue 的结构可以在下图很直观地看到:
虽然这个组件很小,但内部的逻辑已经耦合在一起。一些逻辑专注于计数器,另一些逻辑专注于屏幕宽度。随着组件功能的发展,组织内部逻辑以及定位问题将变得越来越难。
为了应对这些问题,Vue 团队在 Vue 3 中提出了组合式 API。
组合式 API
组合式 API 可以看作是一个 提供了 Vue 核心功能的独立的函数。这些函数用在一个单独的 setup()
选项中,这个选项作为组合式 API 的入口。
<!-- 模板 省略 -->
<script>
export default {
name: "MyComponent",
setup() {
// 入口
},
};
</script>
<!-- 样式 省略 -->
setup()
在组件创建之前执行,并且此时组件的 props 是可用的。
通过组合式 API,我们可以引入独立的函数来在我们的组件中使用 Vue 的核心功能。我们来使用组合式 API 重写上面的计数器和屏幕宽度示例。
<template>
<div class="App">
<Count :count="count" :increment="increment" :decrement="decrement" />
<div id="divider" />
<Width :width="width" />
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from "vue";
import Count from "./components/Count.vue";
import Width from "./components/Width.vue";
export default {
name: "App",
setup() {
const count = ref(0);
const width = ref(0);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const handleResize = () => {
width.value = window.innerWidth;
};
onMounted(() => {
handleResize();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
return {
count,
width,
increment,
decrement,
};
},
components: {
Count,
Width,
},
};
</script>
我们组件的 <template>
部分保持不变,但是现在的 <script>
部分使用带有 setup()
函数的组合式 API。
在 setup()
函数中,我们做了:
使用
ref()
函数定义了count
和width
响应式数据 —— 这个函数接收一个原始类型的值(例如字符串、数字等),并返回一个响应式的对象。我们还定义了自定义的方法
increment()
、decrement()
和handleResize()
。这些函数和我们在之前的选项式 API 的例子中的方法特别相似。我们使用
onMounted()
生命周期钩子函数,在其内部调用了自定义函数handleResize()
,在组件挂载时添加一个事件监听器来监听 resize 事件。类似地,我们使用onBeforeUnmount()
生命周期钩子函数,在组件卸载的时候取消事件监听。响应式数据和函数在
setup()
内部定义,并在返回值中返回,这样可以让它们在组件模板中可以使用。
组合式
在我们之前的代码例子中,你可能有个疑问,就是 setup
到底哪里为开发提供便利了。因为它看上去只是要求我们在函数中声明之前就需要提供的选项。
采用组合式 API 的一大优势是 能够提取和复用组件之间的逻辑。因为我们可以定义全局可用的组合式函数,并且可以在多个组件中使用组合式函数来实现相同的结果。
我们继续使用之前计数器和屏幕宽度的例子,创建一个组合式函数来跨组件复用逻辑。
首先,我们创建一个组合式函数 useCounter
,它封装了计数器功能并返回 count
的当前值、一个 increment()
方法和一个 decrement()
方法。
约定俗成地,组合式函数都以
use
开头来命名。
import { ref } from "vue";
export function useCounter(initialCount = 0) {
const count = ref(initialCount);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
return {
count,
increment,
decrement,
};
}
相似地,我们创建一个组合式函数 useWidth()
,封装了我们应用中的屏幕宽度相关的功能。
import { ref, onMounted, onBeforeUnmount } from "vue";
export function useWidth() {
const width = ref(0);
function handleResize() {
width.value = window.innerWidth;
}
onMounted(() => {
handleResize();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
return {
width,
};
}
在我们 App
组件中,我们现在可以使用组合式函数来达到相同的结果了。
<template>
<div class="App">
<Count :count="count" :increment="increment" :decrement="decrement" />
<div id="divider" />
<Width :width="width" />
</div>
</template>
<script>
import Count from "./components/Count.vue";
import Width from "./components/Width.vue";
import { useCounter } from "./composables/useCounter";
import { useWidth } from "./composables/useWidth";
export default {
name: "App",
components: {
Count,
Width,
},
setup() {
const { count, increment, decrement } = useCounter(0);
const { width } = useWidth();
return {
count,
increment,
decrement,
width,
};
},
};
</script>
通过这些更改,我们的应用的运行方式和之前相同,但是获得了更容易组合使用的配置方式。
在演练场中查看代码通过使用组合式 API 和组合式函数,我们能够将我们的应用分解为更小的可复用的单元,从而实现逻辑分离。
我们直观地了解一下刚才所做的更改,并与最开始的选项式 API 进行比较。
在 Vue 中使用组合式函数可以让我们的组件逻辑很容易地划分为一系列的小块,这时逻辑复用也变得相对更容易了,因为我们不再局限在选项式 API 的框架中,不必再按照选项式 API 的特定选项内组织代码了。
通过使用组合式函数,我们可以灵活地提取并复用重复的逻辑,关注点分离可以使我们能够更专注于每个组合式函数的特定功能,从而让我们的代码可以更加模块化且易于维护。
通过将逻辑分解为更小的可复用的部分,我们可以使用这些组合式函数来组合我们的组件,将必要的功能整合在一起,并且可以避免出现重复的代码。这种方法提高了代码可复用性,并降低了因为代码重复和不一致导致出现风险的几率。
此外,使用组合式 API 可以提高组件的 可读性 和 易理解性,每个组合式函数都封装了特定方面的组件的行为,使其更易于理解和测试。随着代码更加复杂,代码会变得更加结构化并条理清晰,这更有利于团队间协作。
最后,使用组合式 API 构建 Vue 应用可以实现更好的 类型推断。由于组合式 API 可以帮助我们使用变量和标准 JavaScript 函数来处理组件逻辑,因此使用 TypeScript 等静态类型语言构建大型 Vue 应用变得更加容易了。