Vue provide & inject 源码解析
引入
依赖注入是Vue
中负责组件间共享代码的功能,在应用程序的规模增大时,由于组件树的深度会变的很深,使用props
进行组件间传值往往会非常麻烦。因此Vue
推出了这个功能,用来将依赖透传给所有的子组件。
尽管如此,我在实际开发中很少使用这个功能。对我个人而言,使用provide
和inject
进行组件间的代码复用/传值存在着全局变量的缺点,我无法清晰的知道provide
逻辑的发生位置和具体内容,调试和查找引用;而app.provide
虽然可以提供全局级别的属性共享,但是使用app.config.globalProperties
进行全局属性的注册也一样可以达到目的,并且这是一种更常用的做法。
总体来说,我并没有找到依赖注入的最佳实践,它或许有他的用处,但是应用场景实在太窄了。因此,在实际开发中,我还是更倾向于把代码封装为模块,从而进行逻辑封装和代码复用。
说了这么多,不用归不用,源码还是要看一下的,下面就对provide
和inject
两个API
的源码进行解析。
provide
provide
即提供,可以通过在组件内部通过provide
提供依赖,或是通过app.provide
为全局提供依赖:
ts
// main.js
import { createApp } from 'vue'
const app = createApp(/* ... */)
app.provide('message', 'hello')
// App.vue
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
接下来,我们分别分析两者的源码。
provide 函数
provide
的核心逻辑是拿到父组件的provides
对象,使用该对象作为原型创建子组件的provides
对象,并在其中添加依赖。
ts
// apiInject.ts
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T
) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
// by default an instance inherits its parent's provides object
// but when it needs to provide values of its own, it creates its
// own provides object using parent provides object as prototype.
// this way in `inject` we can simply look up injections from direct
// parent and let the prototype chain do the work.
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}
由于在创建子组件时,子组件的provides
默认指向为父组件的provides
(代码见下),因此子组件如果要创建自己的provides
,是不能直接在provides
中添加依赖的,因为这样会影响到父组件。
ts
// 省略了无关代码
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
// 在创建组件实例时,如果组件有父组件,则直接继承父组件的provides
// 如果没有父组件(即组件是根组件),则会继承appContext的provides(即继承app.provides提供的全局依赖)
const instance: ComponentInternalInstance = {
provides: parent ? parent.provides : Object.create(appContext.provides),
}
return instance
}
所以源码采用了使用Object.create
,用父组件的provides
作为原型创建新的对象,并在新的对象中添加依赖。这样子组件的provides
不仅拥有自己的依赖,而且还可以通过原型链的机制查找到父组件提供的依赖。
这里使用原型链最大的好处是,子组件只用维护自己的依赖,而父组件及以上层级提供的依赖只需要借助原型链机制进行查找即可。如果不使用原型链,而是让子组件记录父组件提供的依赖,那么当父组件提供了新的依赖时,还需要遍历修改子组件,这无疑是非常麻烦的。
此外,由于原型链上的属性存在覆盖关系,因此如果子组件和父组件都提供了某个同名的值,子组件的值会覆盖父组件,通过原型链这一点可以非常自然地实现。
app.provide
app.provide
方法在createApp
方法中,创建app
对象时提供,这里提供的依赖在上面提到的createComponentInstance
函数中会提供给根组件,从而可以供所有组件注入,这部分具体内容可以参考我之前写的createApp
解析文章。
ts
const app: App = (context.app = {
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
// 省略其他方法和属性
})
inject
inject
源码的核心逻辑也很简单,本质是拿到当前组件实例,如果当前组件实例不存在,说明是调用的app.runWithContext()
,那么就直接去找全局的provides
;如果当前组件实例存在,则继续判断父组件是否存在:
- 父组件存在:直接拿到父组件的
provides
,由于有上文提到的原型链,因此父组件的provides
中实际上存储了一整条组件通路上的所有提供的依赖 - 父组件不存在:说明是根组件,则去找全局的依赖,即
appContext.provides
关于默认值和工厂函数的逻辑也非常好懂,这里就不赘述了。
ts
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
// also support looking up from app-level provides w/ `app.runWithContext()`
if (instance || currentApp) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides = instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: currentApp!._context.provides
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}
思考与总结
- 使用原型链简化查找:可以使用原型链进行对象属性的继承。源码通过使用原型链搭建了
provides
的链条,使得每个组件只需要维护自己的provides
对象,访问子组件的provides
对象同样可以查找到原型链上的属性(也就是从父组件上继承的属性),而不用向上递归查找。这其实是原型模式的应用,之前从来没在实际开发当中用过原型链,这次学习了