嘿,大家好!今天咱们聊聊 Vue 3 里面一个相当有趣,但有时候又容易被忽略的特性:依赖注入,也就是 provide/inject
。这玩意儿就像 Vue 组件之间的一个秘密通道,让父组件能够直接给后代组件“空投”一些东西,而不用一层层地 props
传递。咱们就来扒一扒它背后的实现原理,看看这个“空投”到底是怎么实现的。
开场:为啥需要依赖注入?
在深入代码之前,先来聊聊为啥需要这玩意儿。想象一下,你有一个特别大的 Vue 应用,组件层级很深。某个组件,比如根组件,需要给它下面某个孙子组件甚至重孙子组件传递一个数据。最简单的办法就是 props
传递,但这意味着所有中间组件都必须接收这个 props
,然后继续往下传。这就有点像传话筒,效率低不说,还污染了中间组件。
依赖注入就是为了解决这个问题。它可以让父组件直接“跳过”中间组件,把数据或者方法直接注入到需要的后代组件中。这就像在组件树上开辟了一条直达的通道,干净利落。
第一部分:provide
的秘密
provide
负责提供依赖。它接收两个参数:一个 key(可以是字符串或 Symbol),和一个 value。这个 value 就是要注入的东西。
咱们先来看看 provide
在 Vue 3 源码中的大致实现(简化版):
// packages/runtime-core/src/apiInject.ts
import { currentInstance } from './component'
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
// 在 setup 之外调用 provide 会报错
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
// 通过原型链向上查找,如果找到父组件的 provides,则说明不是根组件
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
// 初始化 provides
if (provides === parentProvides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// 存储 key-value
provides[key as string] = value
}
}
这段代码做了几件事:
-
检查
currentInstance
:provide
必须在setup
函数里面调用。currentInstance
是一个全局变量,用来指向当前正在执行的组件实例。如果在setup
之外调用provide
,currentInstance
会是null
,报错。 -
获取
provides
: 每个组件实例都有一个provides
属性,用来存储它提供的依赖。 -
原型链的妙用: 这是最关键的地方。如果当前组件的
provides
和父组件的provides
指向同一个对象,说明当前组件还没有提供任何依赖。这时候,会创建一个新的provides
对象,并把父组件的provides
设置为它的原型。重点来了:
provides
是通过原型链连接起来的。这意味着子组件可以沿着原型链向上查找父组件提供的依赖。如果子组件自己也provide
了相同的 key,那么它会覆盖父组件提供的依赖。 -
存储
key-value
: 最后,把key
和value
存储到当前组件的provides
对象中。
用表格总结一下:
步骤 | 描述 |
---|---|
1 | 检查 currentInstance ,确保 provide 在 setup 中调用。 |
2 | 获取当前组件实例的 provides 属性。 |
3 | 如果 provides 和父组件的 provides 相同(说明是第一次 provide ),创建一个新的 provides 对象,并将父组件的 provides 设置为新 provides 的原型。 |
4 | 将 key 和 value 存储到当前组件的 provides 对象中。 |
第二部分:inject
的魔法
inject
负责注入依赖。它接收一个 key(字符串或 Symbol)和一个可选的默认值。如果在组件的祖先组件中找到了这个 key 对应的依赖,就返回这个依赖的值;否则,返回默认值。
再来看看 inject
的大致实现:
// packages/runtime-core/src/apiInject.ts
import { currentInstance } from './component'
export function inject<T>(
key: InjectionKey<T> | string | number,
defaultValue?: T,
treatDefaultAsFactory: boolean = false
): T {
if (!currentInstance) {
if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
return defaultValue as T
}
// 从父组件开始,沿着原型链向上查找 provides
let provides = currentInstance.parent && currentInstance.parent.provides
if (provides && (key as string) in provides) {
// 找到依赖,直接返回
return provides[key as string]
} else if (arguments.length > 1) {
// 没有找到依赖,返回默认值
if (treatDefaultAsFactory && typeof defaultValue === 'function') {
return (defaultValue as Function)()
}
return defaultValue as T
} else if (__DEV__) {
warn(`Injection "${String(key)}" not found`)
}
}
这段代码也做了几件事:
-
检查
currentInstance
: 和provide
一样,inject
也必须在setup
函数里面调用。 -
沿着原型链查找
provides
: 从父组件开始,沿着原型链向上查找provides
对象。 -
找到依赖: 如果在某个
provides
对象中找到了 key 对应的依赖,就直接返回这个依赖的值。 -
没有找到依赖: 如果一直找到根组件都没有找到对应的依赖,并且提供了默认值,就返回默认值。如果没提供默认值,在开发环境下会发出警告。
-
默认值是工厂函数: 如果
treatDefaultAsFactory
为true
并且defaultValue
是一个函数,那么会调用这个函数来获取默认值。这允许你使用一些计算成本较高的默认值,只有在真正需要的时候才计算。
用表格总结一下:
步骤 | 描述 |
---|---|
1 | 检查 currentInstance ,确保 inject 在 setup 中调用。 |
2 | 从父组件开始,沿着原型链向上查找 provides 对象。 |
3 | 如果在某个 provides 对象中找到了 key 对应的依赖,就直接返回这个依赖的值。 |
4 | 如果没有找到依赖,并且提供了默认值,就返回默认值。 如果 treatDefaultAsFactory 为 true 并且 defaultValue 是一个函数,那么会调用这个函数来获取默认值。 |
5 | 如果没有找到依赖,并且没有提供默认值,在开发环境下会发出警告。 |
第三部分:Symbol 的妙用
在实际项目中,我们通常会使用 Symbol 作为 provide/inject
的 key。为啥呢?
因为字符串容易冲突。如果两个毫不相关的组件都使用了相同的字符串作为 key,可能会导致意外的依赖注入。而 Symbol 是唯一的,可以避免这种冲突。
// 创建一个 Symbol 作为 key
const myKey = Symbol('myKey')
// 父组件 provide
provide(myKey, 'Hello from parent!')
// 子组件 inject
const message = inject(myKey) // message 的值为 'Hello from parent!'
第四部分:响应式依赖
provide/inject
还可以提供响应式数据。这意味着如果父组件提供的依赖发生变化,子组件也会自动更新。
实现响应式依赖的关键在于使用 ref
或 reactive
。
// 父组件
import { ref } from 'vue'
setup() {
const count = ref(0)
provide('count', count)
setInterval(() => {
count.value++
}, 1000)
return {}
}
// 子组件
import { inject } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue';
setup() {
const count = inject('count', ref(0)); // 使用 ref 确保是响应式
return {
count
};
}
在这个例子中,父组件提供了一个响应式的 count
,子组件通过 inject
获取这个 count
。当父组件的 count
变化时,子组件也会自动更新。
第五部分:一些注意事项
-
单向数据流: 虽然
provide/inject
允许子组件访问父组件的数据,但仍然要遵循单向数据流的原则。子组件不应该直接修改父组件提供的数据,而是应该通过事件或者其他方式通知父组件进行修改。 -
可维护性: 过度使用
provide/inject
可能会降低代码的可维护性。因为它隐藏了组件之间的依赖关系,使得代码难以理解和调试。只在必要的时候使用它,比如在跨多个层级的组件之间共享配置信息或者状态管理。 -
类型安全: TypeScript 可以帮助你提高
provide/inject
的类型安全。你可以使用InjectionKey
类型来定义 key 的类型,这样可以确保provide
和inject
使用的 key 类型一致。
第六部分:实战例子
假设我们有一个应用,需要实现一个主题切换功能。我们可以使用 provide/inject
来共享主题信息。
// App.vue (根组件)
<template>
<div :class="theme">
<header>
<h1>My App</h1>
</header>
<main>
<MyComponent />
</main>
<footer>
<button @click="toggleTheme">Toggle Theme</button>
</footer>
</div>
</template>
<script>
import { ref, provide } from 'vue';
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
},
setup() {
const theme = ref('light');
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
provide('theme', theme);
return {
theme,
toggleTheme
};
}
};
</script>
<style>
.light {
background-color: #fff;
color: #000;
}
.dark {
background-color: #000;
color: #fff;
}
</style>
// MyComponent.vue (子组件)
<template>
<div :class="theme">
<h2>My Component</h2>
<p>This is a component that uses the theme provided by the parent.</p>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme');
return {
theme
};
}
};
</script>
在这个例子中,根组件 App.vue
提供了一个 theme
依赖,子组件 MyComponent.vue
通过 inject
获取这个 theme
依赖。当根组件的主题切换时,子组件也会自动更新。
总结:依赖注入的艺术
provide/inject
是 Vue 3 中一个强大的特性,可以让你在组件之间共享数据和方法,而无需手动传递 props
。但是,要合理使用它,避免过度使用,遵循单向数据流的原则,并注意类型安全。只有这样,才能充分发挥它的优势,提高代码的可维护性和可读性。
好啦,今天的分享就到这里。希望大家对 Vue 3 的依赖注入有了更深入的理解。下次再遇到类似的需求,就可以自信地使用 provide/inject
来解决问题啦! 大家有什么问题可以提出来,我们一起讨论讨论。