依赖注入

什么是依赖注入?

依赖注入(Dependency Injection)是一种设计模式,用于在组件树中共享数据和方法,而不需要通过 props 一级一级传递。在 Vue3 中,我们使用 provide() 和 inject() 函数来实现依赖注入。

1. 基本用法

provide() 函数用于提供数据,inject() 函数用于注入数据。

示例:基本依赖注入

<!-- 父组件:提供数据 -->
<template>
  <div>
    <h1>父组件</h1>
    <ChildComponent />
  </div>
</template>

<script setup>
import { provide } from 'vue';
import ChildComponent from './ChildComponent.vue';

// 提供静态数据
provide('appName', 'Vue3 App');

// 提供响应式数据
const count = ref(0);
provide('count', count);

// 提供方法
function increment() {
  count.value++;
}
provide('increment', increment);
</script>

子组件或孙组件:注入数据

<template>
  <div>
    <h2>子组件</h2>
    <p>应用名称: {{ appName }}</p>
    <p>计数: {{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

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

// 注入数据
const appName = inject('appName');
const count = inject('count');
const increment = inject('increment');
</script>

2. 注入默认值

可以为 inject() 函数提供默认值,当没有找到对应的数据时,将使用默认值。

示例:注入默认值

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

// 没有提供 appVersion 时,使用默认值
const appVersion = inject('appVersion', '1.0.0');

// 复杂默认值,使用工厂函数
const user = inject('user', () => ({
  name: 'Guest',
  age: 0
}));
</script>

3. 响应式依赖注入

当提供的数据是响应式的(ref 或 reactive),注入后仍然保持响应性。

示例:响应式依赖注入

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

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

// 修改数据
function updateUser() {
  user.value.name = 'Jane';
  user.value.age = 25;
}
provide('updateUser', updateUser);
</script>

子组件

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

// 注入响应式数据
const user = inject('user');
const updateUser = inject('updateUser');

console.log(user.value.name); // John

// 调用注入的方法
updateUser();
console.log(user.value.name); // Jane
</script>

4. 只读注入

可以使用 readonly() 函数将提供的数据设置为只读,防止子组件修改。

示例:只读注入

<!-- 父组件 -->
<script setup>
import { reactive, readonly, provide } from 'vue';

const state = reactive({ count: 0 });

// 提供只读数据
provide('state', readonly(state));

// 提供修改方法
function increment() {
  state.count++;
}
provide('increment', increment);
</script>

子组件

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

const state = inject('state');
const increment = inject('increment');

// 尝试修改只读数据
state.count++;
// 警告:Cannot assign to 'count' because it is a read-only property

// 只能通过提供的方法修改
increment(); // 正确
</script>

5. 依赖注入的作用域

provide() 提供的数据只在其组件树的后代组件中可用。

示例:作用域示例

<!-- Grandparent.vue -->
<script setup>
import { provide } from 'vue';

provide('grandparentData', '来自祖父组件的数据');
</script>

Parent.vue

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

provide('parentData', '来自父组件的数据');
// 可以覆盖祖父组件提供的数据
provide('grandparentData', '覆盖祖父组件的数据');
</script>

Child.vue

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

// 注入数据
const grandparentData = inject('grandparentData');
const parentData = inject('parentData');

console.log(grandparentData); // 覆盖祖父组件的数据
console.log(parentData); // 来自父组件的数据
</script>

6. 组合式 API 与选项式 API 的混合使用

可以在组合式 API 中提供数据,在选项式 API 中注入数据,反之亦然。

示例:混合使用

<!-- 组合式 API 提供数据 -->
<script setup>
import { provide } from 'vue';

provide('message', 'Hello from composition API');
</script>

选项式 API 注入数据

<script>
export default {
  inject: ['message'],
  mounted() {
    console.log(this.message); // Hello from composition API
  }
}
</script>

7. 依赖注入的最佳实践

  • 用于在组件树中共享全局状态或配置
  • 用于共享工具函数或服务
  • 用于跨多级组件传递数据,避免 props 钻取
  • 为注入的数据提供默认值,提高组件的健壮性
  • 对于响应式数据,考虑使用 readonly() 防止子组件意外修改
  • 使用命名空间避免命名冲突,如 'user.name'、'user.age'

8. 依赖注入 vs Props

特性 依赖注入 Props
传递方式 跨多级组件 父子组件
显式性 隐式,依赖关系不明确 显式,依赖关系明确
响应性 支持响应式数据 支持响应式数据
默认值 支持 支持
类型检查 需要手动实现 内置类型检查
适用场景 全局状态、工具函数、服务 父子组件通信

9. 依赖注入的注意事项

  • 依赖注入会使组件之间的关系变得不明确,增加调试难度
  • 过度使用依赖注入会导致组件耦合度增加
  • 对于复杂的全局状态,考虑使用状态管理库(如 Pinia)
  • 注入的数据是响应式的,修改会影响所有注入该数据的组件