前言
在项目中不可避免的要进行组件间的通信,provide和inject是其中的一种方式,也叫做依赖注入 ,用于父组件向其子代组件中传递数据。可以将其理解为通过props
传值的方式在多级嵌套组件下的改进。 笔者想了解下他是如何实现的,但经过简单的检索后没有找到满意的文章,那就自己去看看源码,顺便记录一下。
用法
provide和inject的用法这里就不再赘述了,我们主要来关注vue3是如何来实现的这套api的
源码分析
源码地址:github.com/vuejs/core/...
打开源码看下,其实实现并不复杂
provide
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
}
}
简单梳理下provide
的实现逻辑:上来就是先判断当前组件实例是否存在,若为null
则说明provide
没有使用在组件的声明周期中,在开发模式下发出warning。
反之若组件实例存在,则获取组件实例上的provides
属性和当前组件的父组件的provides。接下来进行一个判断parentProvides === provides
,判断是否是在初始状态下声明provide注入的,如果为真则要为当前组件实例上创建一个独立的 provides
对象,要注意是用Object.create()
来创建新对象的,也就是常说的原型继承,把父组件实例的provides
作为子组件provides
的原型,然后再对它进行赋值操作。
至于为什么一定要有这个
currentInstance
呢?因为provide
所注入的数据都存放在currentInstance
中,而inject
读取数据也依赖于它
inject
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.`)
}
}
inject
这里做的事情也很简单了,用in
操作符来判断要注入的数据是否存在于当前组件实例的provides
的原型链中,如果有则返回之。
这里还有一个点可以聊一下,
provide
api本身只能用于组件中,但vue的应用实例app下也有一个app.provide
方法,这个方法就是用于在非组件上下文中提供数据,比如应用上下文。这也是pinia中所用到的一个点,将pinia
实例通过app.provide
作为全局变量,使得在各个组件中调用useStore
时,useStore
内部可以通过inject
获取到pinia
实例,并把创建的store挂载到pinia._s
上。所以我们可以看到inject
中会尝试获取currentApp!._context.provides
小结
通过对源码的分析,我们知道整个依赖注入的实现原理其实就是:provide
在每个组件实例上挂载一个provides
对象,并且保持着跟父子组件相同嵌套关系的原型链,然后inject
在从原型链中去查找这个值并返回。