Vue3 依赖注入(provide 和 inject)
- [1. Prop 逐级透传问题(依赖注入出现的原因)](#1. Prop 逐级透传问题(依赖注入出现的原因))
- [2. 快速入门](#2. 快速入门)
-
- [2.1 完整示例](#2.1 完整示例)
- [3. 使用细节](#3. 使用细节)
-
- [3.1 Provide(提供)](#3.1 Provide(提供))
-
- [3.1.1 provide() 的两个参数(注入名,注入值)](#3.1.1 provide() 的两个参数(注入名,注入值))
- [3.1.2 应用层 Provide(全局依赖,编写插件很有帮助)](#3.1.2 应用层 Provide(全局依赖,编写插件很有帮助))
- [3.2 Inject(注入)](#3.2 Inject(注入))
-
- [3.2.1 inject() 函数的使用方式](#3.2.1 inject() 函数的使用方式)
- [3.2.2 注入最近父组件提供的数据](#3.2.2 注入最近父组件提供的数据)
- [3.2.3 注入默认值](#3.2.3 注入默认值)
- [3.3 和响应式数据配合使用](#3.3 和响应式数据配合使用)
-
- [3.3.1 数据和方法包装在一个对象中(简写)](#3.3.1 数据和方法包装在一个对象中(简写))
- [3.3.2 子组件直接修改 provide 的数据(不推荐,也不合理)](#3.3.2 子组件直接修改 provide 的数据(不推荐,也不合理))
- [3.3.3 readonly 保证注入数据不被直接修改](#3.3.3 readonly 保证注入数据不被直接修改)
- [3.4 使用 Symbol 作为注入名(避免潜在冲突)](#3.4 使用 Symbol 作为注入名(避免潜在冲突))
1. Prop 逐级透传问题(依赖注入出现的原因)

在之前我们学习的知识中,可以得知,在 Vue 中通常使用 props,来实现父组件向子组件传递数据的过程。
但是,我们也会发现一个问题,如果组件嵌套太深,并且深层子组件需要一个较远的祖先组件的部分数据,如果仍然使用 props 进行逐级传递,则代码会变得十分冗余,并且状态难以管理(修改一个,需要处处修改)。
因此,Vue 提供了一个方式,provide 和 inject 可以帮助我们解决 "prop 逐级透传" 问题:
(1)使用 provide,让一个父组件相对于其所有的后代组件,作为依赖提供者;
(2)使用 inject,让任意深度的子组件,可注入由父组件提供给整条链路的依赖。
2. 快速入门
其实在我之前《Vue3 模板引用------ref》一章中的 2.2.2 使用 provide 和 inject(比较少用,跨级别父子通信) 这一小节处,依赖注入就曾经被当做拓展进行说明过。在这一章节,我们将会针对细节,进行进一步的补充。
关键代码:
javascript
<script setup>
import { ref, provide } from 'vue'
import Child from '@/components/Child.vue'
const number = ref(10)
const parentMethod = () => {
console.log('调用了父组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script>
javascript
<script setup>
import { inject } from 'vue';
// 获取任意级别父组件的变量和方法
const parentNumber = inject('number')
const parentMethod = inject('parentMethod')
const callParentMethod = () => {
if(parentMethod) {
parentMethod();
} else {
console.log('父组件中没有该方法');
}
}
</script>
2.1 完整示例
让我们来看一个示例。来理解依赖注入的使用方式。

父组件 App.vue:
javascript
<template>
<div class="parent">
<div>这是父组件</div>
<div>{{ number }}</div>
<Child />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import Child from '@/components/Child.vue'
const number = ref(10)
const parentMethod = () => {
console.log('调用了父组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script>
<style lang="scss" scoped>
.parent {
color: red;
border: 1px solid red;
padding: 10px;
}
</style>
子组件Child.vue:
javascript
<template>
<div class="child">
<div>这是子组件</div>
<GrandChild />
</div>
</template>
<script setup>
import GrandChild from './GrandChild.vue';
</script>
<style lang="scss" scoped>
.child {
color: green;
border: 1px solid green;
padding: 10px;
}
</style>
第二级子组件(这里简称 "孙子组件") grandChild.vue:
javascript
<template>
<div class="grandcCild">
<div>这是孙子组件</div>
<div>{{ parentNumber }}</div>
<button @click="callParentMethod">调用祖先中最近的parentMethod方法</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 获取任意级别父组件的变量和方法
const parentNumber = inject('number')
const parentMethod = inject('parentMethod')
const callParentMethod = () => {
if(parentMethod) {
parentMethod();
} else {
console.log('父组件中没有该方法');
}
}
</script>
<style lang="scss" scoped>
.grandcCild {
color: blue;
border: 1px solid blue;
padding: 10px;
}
</style>

3. 使用细节
3.1 Provide(提供)
3.1.1 provide() 的两个参数(注入名,注入值)
要为组件后代提供数据,需要使用到 provide() 函数:
javascript
<script setup>
import { ref, provide } from 'vue'
const number = ref(10)
const parentMethod = () => {
console.log('调用了父组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script>
provide 函数提供两个参数:
(1)第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值或者方法。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
(2)第二个参数是提供的值,值可以是任意类型。可以是一个字符串、一个响应式ref、一个 function 等。
3.1.2 应用层 Provide(全局依赖,编写插件很有帮助)
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
javascript
const app = createApp(App)
app.provide('appMessage', 'hello vue3 provide')
在应用级别提供的数据在该应用内的所有组件中都可以注入。比如,我们在 2.1 完整示例 的子组价中进行注入使用:


另外,应用层 provide 在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
3.2 Inject(注入)
3.2.1 inject() 函数的使用方式
要注入上层组件提供的数据,需使用 inject() 函数:
javascript
<script setup>
import { inject } from 'vue';
// 获取任意级别父组件的变量和方法
const parentNumber = inject('number')
const parentMethod = inject('parentMethod')
const callParentMethod = () => {
if(parentMethod) {
parentMethod();
} else {
console.log('父组件中没有该方法');
}
}
</script>
值得注意的是,对于注入的函数,最好先进行判空,避免不必要的警告或报错。
3.2.2 注入最近父组件提供的数据
如果多个级别的父组件都使用provide提供了相同名称的变量或方法,子组件在调用 inject 获取时只会获取到最近的组件的变量或方法。
比如我们对 2.1 完整示例 中的子组件 Child.vue 进行修改,也添加 number 和 parentMethod:
javascript
<template>
<div class="child">
<div>这是子组件</div>
<div>{{ number }}</div>
<GrandChild />
</div>
</template>
<script setup>
import { ref, provide } from 'vue';
import GrandChild from './GrandChild.vue';
const number = ref(20);
const parentMethod = () => {
console.log('调用了子组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script>
<style lang="scss" scoped>
.child {
color: green;
border: 1px solid green;
padding: 10px;
}
</style>

3.2.3 注入默认值
通常情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。但是,如果注入名没有任何祖先组件提供,则会出现警告。比如:


如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:
javascript
const testInject = inject('testInject', '这是一个默认值')

在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
javascript
const value = inject('key', () => new ExpensiveClass(), true)
第三个参数表示默认值应该被当作一个工厂函数。
3.3 和响应式数据配合使用
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数。
在 2.1 完整示例 中的 parentMethod 方法就是。
3.3.1 数据和方法包装在一个对象中(简写)
但是我们仍然能够对代码进行优化简写。在示例中,我们每个数据和方法都使用了一次provide,代码相对冗余。事实上,我们可以将其都包装在一个对象中。比如:

provide(提供)依赖的父组件 App.vue:
javascript
<template>
<div class="parent">
<div>这是父组件</div>
<div>{{ number }}</div>
<Child />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import Child from '@/components/Child.vue'
const number = ref(10)
const addNumber = () => {
console.log('调用了父组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', {
number,
addNumber
})
</script>
<style lang="scss" scoped>
.parent {
color: red;
border: 1px solid red;
padding: 10px;
}
</style>
子组件 Child.vue:
javascript
<template>
<div class="child">
<div>这是子组件</div>
<GrandChild />
</div>
</template>
<script setup>
import GrandChild from './GrandChild.vue';
</script>
<style lang="scss" scoped>
.child {
color: green;
border: 1px solid green;
padding: 10px;
}
</style>
inject(注入)依赖的孙子组件 GrandChild.vue:
javascript
<template>
<div class="grandcCild">
<div>这是孙子组件</div>
<div>{{ number }}</div>
<button @click="addNumber">调用祖先中最近的addNumber方法</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 获取任意级别父组件的变量和方法
const { number, addNumber } = inject('number')
</script>
<style lang="scss" scoped>
.grandcCild {
color: blue;
border: 1px solid blue;
padding: 10px;
}
</style>
3.3.2 子组件直接修改 provide 的数据(不推荐,也不合理)
我们在子组件创建一个函数,观察是否能够修改祖先组件provide的 ref 变量。

父组件 App.vue:
javascript
<template>
<div class="parent">
<div>这是父组件</div>
<div>{{ number }}</div>
<Child />
</div>
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
import Child from '@/components/Child.vue'
const number = ref(10)
const addNumber = () => {
console.log('调用了父组件方法');
number.value++
}
// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('addNumber', addNumber)
</script>
<style lang="scss" scoped>
.parent {
color: red;
border: 1px solid red;
padding: 10px;
}
</style>
子组件 Child.vue:
javascript
<template>
<div class="child">
<div>这是子组件</div>
<GrandChild />
</div>
</template>
<script setup>
import GrandChild from './GrandChild.vue';
</script>
<style lang="scss" scoped>
.child {
color: green;
border: 1px solid green;
padding: 10px;
}
</style>
孙子组件 GrandChild.vue:
javascript
<template>
<div class="grandcCild">
<div>这是孙子组件</div>
<div>{{ number }}</div>
<button @click="addNumber">调用祖先中最近的addNumber方法</button>
<button @click="subNumber">当前组件的减法</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 获取任意级别父组件的变量和方法
const number = inject('number')
const addNumber = inject('addNumber')
function subNumber() {
number.value--
console.log('孙子组件的减法', number)
}
</script>
<style lang="scss" scoped>
.grandcCild {
color: blue;
border: 1px solid blue;
padding: 10px;
}
button{
margin-right: 10px;
}
</style>

很神奇,Vue 并没有在这件事上做任何限制,意味着provide变量,在子组件上也是可以进行修改,并且影响到整个链条的。
3.3.3 readonly 保证注入数据不被直接修改
如果你想保证注入的数据不被子组件直接更改,可以使用 readonly 函数。

其实就是在 3.3.2 的例子中,给提供的数据包个 readonly 函数。

可以看到,子组件无法修改父组件provide的readonly数据。强行修改会有警告。
但是,值得注意的是,虽然变量 number 提供给 子组件时是readonly,但是在提供方组件中只是普通的ref,因此可以进行修改。所以如果子组件使用了父组件提供的方法,仍然是能够进行修改的。

3.4 使用 Symbol 作为注入名(避免潜在冲突)
如果我们正在构建一个大型应用或者编写一个提供给他人使用的组件库,可能会包含非常多的依赖提供。
此时非常建议使用 Symbol 作为组件名,并且推荐在一个单独的文件进行导出,从而避免潜在的冲突。
比如:
javascript
// key.js
export const myInjectionKey = Symbol()
javascript
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, {
/* 要提供的数据 */
})
javascript
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
上一章 《Vue3 插槽》
下一章 《Vue3 重构待办事项(主要练习组件化)》