导读

Vue有一个注入全局上下文的机制,叫provide/inject,其可以向子级组件注入一些属性,无论嵌套得有多深。其效果和React的Context类似。本文将通过具体的源码详细说明其原理实现。

prop drilling diagram

下方所述代码在 /core/packages/runtime-core/src/apiInject.ts

provide

provide 接受两个参数,key和value,也就是注入数据的key值和具体注入的数据,key可接受string和Symbol类型。

先贴出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
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,最终开发者使用provide注入的数据都会设置在其身上,从``provide开始往下的组件树,子组件使用provide时将同样使用父级组件的provides属性来创建自己的provides`。

在这段代码中,需要注意的是Object.create()的使用。其传入一个现有的对象作为原型,创建一个新的对象。也就是说子级组件的provides从父级继承而来,是一个新的对象,保留父级组件provides属性的同时,也不会污染到父级组件provides,实现了隔离。但请注意:子组件的provides在创建时直接复用父级组件的provides或应用上下文对象的provides(当组件是根组件时),也就是按需隔离,也就是使用provide时才使用Object.create()通过原型链实现隔离,否则每一子组件从创建开始就创建自己的provides,随着组件嵌套,这个原型链也将增长,进一步影响到性能。

inject

inject接受三个参数,第一个参数就是provide使用的key,第二个就是上游父组件没有使用provide提供数据时默认的值,第三个则是是否默认第二个参数是函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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) {
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp
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) {
// 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.`)
}
}

首先通过currentInstance拿到组件渲染实例,同时,由于vue提供了函数式组件的的能力,将通过currentRenderingInstance拿到组件实例。通过if(instance || currentApp)判断当前是否有组件实例,如果没有就观察是否使用app.runWithContext注入了上下文,如果注入了currentApp有值。而app.runWithContext通常和createApp()使用有关。

1
import {createApp} from "vue"

这里可以看看笔者之前的一篇文章:Cesium+Vue3实现可跟踪的点位详情弹窗,其就是利用到createApp()这个函数。

1
2
3
4
5
6
7
const provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined

上述的一大串的三元表达式,当createApp存在,此时是一个应用的概念,其组件componentsprovidesexports属性都保存在内部的上下文属性_context上,取出provides值;当组件示例存在但是没有父组件,意味着其为根组件,那么就从应用的上下文对象appContext取出provides。往下的逻辑就是取出provides对应的属性,如果没有属性且提供了默认值的情况,则使用默认值,否则返回的就是undefined。当然,在开发模式下,如果找不到也没有默认值,就打印警告。

总结

provide/inject给Vue提供了上下文对象的能力,无论组件嵌套多么深。其原理就是在组件实例或者上下文对象上设置了provides属性,子级组件创建时将复用父级组件的provides,并在需要时通过Object.create()原型继承父级provides。消费provides对象上的数据时,通过inject取出。