provide / inject
provide 和 inject
为组件树之间的不同层级提供了一种通信机制,它允许一个祖先组件提供一个可被其所有后代组件使用的值,而不必将这个值通过 Props
逐级传递下去
一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
Provide(提供依赖)
Provide() 函数,为后代组件提供数据
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
如果不使用setup,需要保证 provide()
是在setup()
同步调用的:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
同步调用在这里的含义是指 provide 函数调用必须直接发生在 setup 函数的执行流程中,而不是在任何异步操作(如 setTimeout、Promise.then 或 async 函数等)的回调中。
因为Vue的 setup 函数在组件初始化时同步执行,需要确保provide 提供的状态是可以马上供其子组件 inject 依赖的
Error示例
import { provide } from 'vue';
export default {
setup() {
// 错误示范:将 provide 放在异步操作中
setTimeout(() => {
provide('message', 'hello!'); // 子组件可能无法接收到这个值
}, 1000);
// 其他异步逻辑...
},
};
// 在 provide 被调用之前,
// 子组件的 setup 函数可能已经执行完毕,这时候子组件尝试 inject 的值将不会存在
Inject(注入)
inject() 函数
,注入上层组件提供的数据
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果提供的值是一个 ref
,注入进来的会是该 ref 对象,而不会自动解包为其内部的值.
如果没有使用 <script setup>
,inject()
需要在 setup()
内同步调用
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
注入默认值
inject 提供了使用默认值的选项,如果在组件树中未找到提供的值,将使用默认值。
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
工厂函数创建默认值, 第三个参数true
表示默认值应该被当作一个工厂函数
const value = inject('key', () => new ExpensiveClass(), true)
工厂函数作为默认值时,只有在没有找到提供的值时才会执行,减少不必要的性能开销
Symbol作为键
推荐使用 Symbol 作为 provide/inject 的键
,这样可以确保键的唯一性,避免字符串键可能出现的命名冲突
具体示例
Provide(key, value)
第一个参数: 注入的键名,string | Symbol
第二个参数:给后代提供的值,任意类型
响应式ref也ok。
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
Inject(key, defaultValue, booleanTrue)
key
- 要注入的键名;这是唯一必须提供的参数。这个键必须与祖先组件 provide 时使用的键相对应
defaultValue(可选)
- 如果没有找到提供的键,将使用的默认值。
true(可选)
- 仅当第二个参数为函数时才有用。如果为 true,将把该函数视为工厂函数并调用它以获取默认值。
- 意味着你可以延迟创建默认值,只有在需要时才实例化,避免不必要的性能开销
<script setup>
import { inject } from 'vue'
// 如果key没有提供,msg => undefined
const msg = inject('key')
// 如果key没有提供,msgDefault => defaultValue
const msgDefault = inject('key', 'defaultValue')
// 如果'providedKey'没有提供,将调用 () => new ExpensiveObject() 来获取默认值
const injectedValue = inject('providedKey', () => new ExpensiveObject(), true);
</script>
响应式数据的修改
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<!-- 相当于子组件调用父组件方法修改父组件传来的数据 -->
<button @click="updateLocation">{{ location }}</button>
</template>
如果你想确保提供的数据不能被注入方的组件更改,你可以使用readonly()
来包装提供的值。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
Symbol作为键名
但如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
key.ts
推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.ts
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import type { myInjectionKey } from './keys.ts'
provide(myInjectionKey, { /*
要提供的数据
*/ });
// 注入方组件
import { inject } from 'vue'
import type { myInjectionKey } from './keys.ts'
const injected = inject(myInjectionKey)