Vue3

Composition API 逻辑抽离与响应式技巧

全面讲解 Vue3 中 Composition API 的逻辑抽离方法,包括 composable 函数、provide/inject 用法、响应式模块封装及常见陷阱与调试技巧

🎯 引言

学会这篇文章,你能够熟练运用 Vue3 组合式 API 将复杂逻辑拆分成可复用的模块,让组件结构更清晰,维护更轻松。你将理解如何使用 composable 函数抽离逻辑,掌握 provide/inject 的灵活传值方式,封装响应式模块以便复用,并避开常见的响应式陷阱,同时掌握高效调试技巧。无论是个人项目还是团队协作,都能显著提升开发效率和代码质量。


🌀 Composition API 逻辑抽离的意义

在 Vue2 里,我们习惯用 mixin 来复用逻辑,但 mixin 会带来命名冲突和阅读困难。Vue3 的 Composition API 鼓励将逻辑抽离到独立的函数中(composable),让代码更加模块化、可读性更强且复用方便。我们来一步步深入讲解这些核心技巧。


🧩 抽离逻辑到 composable 函数(自定义 hooks)

在 Vue3 中,一个 composable 函数本质是一个封装了响应式数据、计算属性和方法的普通函数。它让你把组件中的逻辑单独拆出来,方便复用和测试。

useCounter.js
export function useCounter() {
    const count = ref(0);
    function increment() {
        count.value++;
    }
    return { count, increment };
}
Example.vue
<template>
    <div>
        <p>计数:{{ count }}</p>
        <button @click="increment">加一</button>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useCounter } from './useCounter.js';
const { count, increment } = useCounter();
</script>

这个例子中,我们把计数器逻辑提取到 useCounter 函数中,组件调用时只需解构返回的响应式数据和方法。这样的函数能被多个组件共享,避免重复代码。

composable 函数名字通常以 “use” 开头,符合 Hooks 风格,增强语义清晰度。

组合多个 composable

通常一个组件会调用多个 composable,它们不会相互干扰,这点比 mixin 更加安全。

const { count, increment } = useCounter();
const { user, login } = useUser();

🧑‍🤝‍🧑 provide inject 在组合式 API 中的使用

provideinject 是 Vue 提供的跨级组件通信方案,有时也会用在逻辑层面共享状态。用 Composition API 时,它们依然有效,且写法更加简洁。

Parent.vue
<template>
    <Children />
</template>

<script setup>
import { provide, ref } from 'vue';
import Children from './Children.vue';

const theme = ref('dark');
provide('theme', theme);
</script>
Children.vue
<template>
    <div>当前主题是:{{ theme }}</div>
</template>

<script setup>
import { inject } from 'vue';
const theme = inject('theme');
</script>

在组合式 API 中,provideinject 依然是响应式的。子组件通过 inject 访问的响应式对象,会保持响应式更新。

provideinject 适合祖孙组件或复杂组件树中的状态共享,避免层层传 props。

在 composable 中封装 provide inject

你也可以将 provideinject 封装到 composable 中,实现逻辑复用:

useTheme.js
import { provide, inject, ref } from 'vue';

export function provideTheme() {
    const theme = ref('light');
    provide('theme', theme);
    return theme;
}

export function useTheme() {
    const theme = inject('theme');
    if (!theme) throw new Error('useTheme 必须在 provideTheme 作用域内使用');
    return theme;
}

这样,父组件调用 provideTheme,子组件调用 useTheme,保证了状态的共享且结构清晰。


🔄 响应式模块的封装与复用

Vue3 的响应式系统基于 Proxy,封装响应式模块时我们注意保持响应式引用完整,避免解构丢失响应性。

封装一个响应式表单状态

假设有一个登录表单,可以抽离成一个响应式表单模块:

import { reactive, computed } from 'vue';

export function useLoginForm() {
    const form = reactive({
        username: '',
        password: '',
    });

    const isValid = computed(() => {
        return form.username.length > 0 && form.password.length > 0;
    });

    function reset() {
        form.username = '';
        form.password = '';
    }

    return { form, isValid, reset };
}

组件调用:

<template>
    <form @submit.prevent="submit">
        <input v-model="form.username" placeholder="用户名" />
        <input type="password" v-model="form.password" placeholder="密码" />
        <button :disabled="!isValid">登录</button>
    </form>
</template>

<script setup>
import { useLoginForm } from './useLoginForm';

const { form, isValid, reset } = useLoginForm();

function submit() {
    alert(`用户名:${form.username},密码:${form.password}`);
    reset();
}
</script>

避免响应式陷阱

  • 避免解构响应式对象,比如 const { username } = form,这样解构后 username 就不是响应式了。应直接使用整体响应式对象或用 toRefs 处理。
import { reactive, toRefs } from 'vue';
const form = reactive({ username: '张三' });
// 错误示范
const { username } = form;
// 正确示范
const { username } = toRefs(form);
  • 对数组和对象的修改要通过响应式 API,不要直接替换响应式对象,否则会丢失响应式。

🧾 小节总结

  • composable 函数(自定义 hooks)能把逻辑拆分为独立模块,方便复用和维护。
  • provide/inject 依然是组合式 API 中跨级传值的利器,且可以封装到 composable 中。
  • 正确封装响应式模块,避免解构响应式对象,保证响应式完整性。
  • 掌握响应式陷阱,结合 Vue Devtools 和调试技巧,快速定位和解决问题。

❓ 知识问答(Q&A)

Q:什么是 composable 函数?

A:composable 函数是封装了响应式状态和逻辑的普通函数,用于逻辑复用和拆分,类似于 React 中的 hooks。

Q:为什么不要直接解构 reactive 对象?

A:因为解构会导致响应式丢失,推荐使用 toRefs 转化为响应式引用。

Q:provide/inject 的响应性如何保证?

A:通过 provide 提供的响应式变量在 inject 时依然保持响应性,能自动更新。


🧪 小练习

练习 1

编写一个 useTodoList composable,实现添加、删除和标记完成的功能,组件调用它来管理 todo 列表。

<template>
    <div>
        <input v-model="newTodo" placeholder="新任务" />
        <button @click="addTodo">添加</button>
        <ul>
            <li v-for="(todo, index) in todos" :key="index">
                <label>
                    <input type="checkbox" v-model="todo.done" />
                    <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">{{
                        todo.text
                    }}</span>
                </label>
                <button @click="removeTodo(index)">删除</button>
            </li>
        </ul>
    </div>
</template>

<script setup>
// 在这里导入并使用 useTodoList
</script>

练习2

封装一个主题切换的 composable,父组件通过 provide 提供当前主题,子组件通过 inject 切换和展示主题。


🎉 恭喜你已经掌握 Vue3 组合式 API 中逻辑抽离、provide/inject 使用、响应式模块封装与调试技巧啦!