Provide、Inject
可以帮助我们解决 "prop 逐级透传" 的问题,那什么是 "prop 逐级透传" 呢?
如下图 <Root>
想要把参数传递到 <DeepChild>
的时候 <Footer>
可能根本不关心这些 props 是什么,但为了使 <DeepChild>
能访问到它们,仍然需要定义并向下传递
如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为"prop 逐级透传"
(官网的图画的很好,现在是我的了)
而 Provide、Inject
则可以帮助我们解决这一问题
Provide、Inject
一般是成对出现的,由 Provide
在组件中提供数据、使用 Inject
在其任何后代的组件树,无论层级有多深 都可以获取由 Provide
提供的数据(Provide 提供的不一定是数据也可以是函数)
总结:Provide、Inject 常于祖先组件与后代组件之间的通信,但不适用于任意兄弟组件间通信。
Provide、Inject API
Inject
无法无法获取到同一组件内 Provide
提供的值但我想也没人会这么写吧!🤪
Provide
提供一个值,可以被后代组件注入
ts
function provide<T>(key: InjectionKey<T> | string, value: T): void
provide 第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值
建议使用 symbol 来定义 key 并将 key 放在一个单独的文件中,这样它就可以被多个组件导入
ts
<script setup>
import { ref, provide } from 'vue'
import { countSymbol } from './injectionSymbols'
// 提供静态值
provide('path', '/project/')
// 提供响应式的值
const count = ref(0)
provide('count', count)
// 提供时将 Symbol 作为 key
provide(countSymbol, count)
</script>
Inject
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值
ts
// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined
// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
// 使用工厂函数
function inject<T>(
key: InjectionKey<T> | string,
defaultValue: () => T,
treatDefaultAsFactory: true
): T
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会"覆盖"链上更远的组件所提供的值。如果没有能通过 key 匹配到值,
inject()
将返回undefined
,除非提供了一个默认值。
inject 不设置默认值,拿不到数据返回 undefined
, 设置默认值,拿不到数据返回 "设置默认值"
对某些创建起来比较复杂的值 inject 第二个参数可以是一个工厂函数但同时第三个参数需要设置为 true
ts
<script setup>
import { inject } from 'vue'
import { countSymbol } from './injectionSymbols'
// 注入不含默认值的静态值
const path = inject('path')
// 注入响应式的值
const count = inject('count')
// 通过 Symbol 类型的 key 注入
const count2 = inject(countSymbol)
// 注入一个值,若为空则使用提供的默认值
const bar = inject('path', '/default-path')
// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('factory', () => new ExpensiveObject(), true)
</script>
为 provide / inject 标注类型
官网写的比我好,看官网!
Provide、Inject 源码阅读
看完使用了,上点强度看下源码是如何实现的!
源码位置 packages/runtime-core/src/apiInject.ts
provide、inject 写在同一个文件内
先来看下文件中引入的一些依赖
ts
import { isFunction } from '@vue/shared'
import { currentInstance } from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { currentApp } from './apiCreateApp'
import { warn } from './warning'
- isFunction:判断传入的值是否是一个函数
- currentInstance:表示当前正在执行 setup() 函数的组件实例,在组件的 setup() 函数执行期间会被设置
- currentRenderingInstance:表示当前正在渲染的组件实例,主要用于函数式组件的上下文中,在组件的 render 函数执行期间会被设置
- currentApp :当前正在运行的 Vue 应用实例,在调用 createApp() 创建 1.warn:控制台警告日志输出
Provide
ts
interface InjectionConstraint<T> {}
export type InjectionKey<T> = symbol & InjectionConstraint<T>
/**
* provide 函数用于提供可注入的依赖
* @param key - 依赖的键,可以是 symbol、字符串或数字
* @param value - 要提供的值
*/
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T,
): void {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
/** 官方注释
* 默认情况下,组件实例继承其父组件的 provides 对象
* 但当需要提供自己的值时,会创建一个新的 provides 对象
* 并将父组件的 provides 对象作为原型
* 这样在 inject 中可以直接查找直接父组件的注入,并让原型链完成剩下的工作
*/
// 获取当前实例的 provides
let provides = currentInstance.provides
// 获取父组件的 provides
// 这里可能的值是 null or 一个对象
const parentProvides = currentInstance.parent && currentInstance.parent.provides
// 如果一致就表示这是当前组件第一次调用 provide 提供数据
// 使用 parentProvides 创建一个新的 provides 对象
// 这样既可以子组件不会修改父组件的 provides,又可以让子组件可以访问父组件的 provides (需要理解 js 原型、对象引用)
if (parentProvides === provides) {
// provides 和 currentInstance.provides 指向同一个对象,当修改这个对象的属性时,无论通过哪个引用修改,都会反映在这个对象上
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS 不允许使用 symbol 作为索引类型,所以这里转换为 string
provides[key as string] = value
}
}
因为
provides
和currentInstance.provides
指向的是同一个对象,- 所以当我们修改provides
对象时,currentInstance.provides
也会同步更新
Inject
- currentApp._context.provides: 应用上下文的 provides
- instance.parent:当前组件的父组件实例,组件挂载时确定
- instance.vnode.appContext:Vue 应用的根级上下文对象本质上与 `currentApp._context 相同
ts
// 为 inject 函数定义多个重载签名,以支持不同的使用场景
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T,
treatDefaultAsFactory?: false,
): T
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T | (() => T),
treatDefaultAsFactory: true,
): T
/**
* inject 函数用于注入依赖
* @param key - 要注入的依赖的键
* @param defaultValue - 默认值,当找不到注入值时使用
* @param treatDefaultAsFactory - 是否将默认值作为工厂函数处理
*/
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false,
) {
// 获取当前实例,如果在函数式组件中,则回退到 currentRenderingInstance
const instance = currentInstance || currentRenderingInstance
// 同时支持通过 app.runWithContext() 查找应用级别的 provides
if (instance || currentApp) {
// 确定 provides 来源:
// 1. 如果存在 currentApp,使用应用上下文的 provides
// 2. 否则,如果是根组件实例,使用 vnode 的 appContext provides
// 3. 否则,使用父组件的 provides
const provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
if (provides && (key as string | symbol) in provides) {
// 如果在 provides 中找到对应的 key,返回其值
return provides[key as string]
} else if (arguments.length > 1) {
// 如果提供了默认值
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy) // 如果默认值是工厂函数,则调用它
: defaultValue // 否则直接返回默认值
} else if (__DEV__) {
// 如果既没有获取到对应的 key 的数据又没有默认值控制台抛出警告
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}
参考
文中部分代码来自官网,我宣布 vue 官网是写的最棒的!