React Context API 提供了一种 Provider 模式,用以在组件树中的多个任意位置的组件之间共享属性,从而避免必须在多层嵌套的结构中层层传递 props。其围绕 Context 的概念,分别提供了 Provider 和 Comsumer 两种对象。
虽然 API 不同,且更倾向用于插件,但 Vue 中同样提供了 Provider 模式。比如 Vue 2.x 文档中对此的描述是:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。...... provide 和 inject 主要在开发高阶插件/组件库时使用。
Vue 3.x 的组合式 API 中也提供了两个类似的独立函数,Composition API RFC 中写道:
许多 Vue 的插件都向 this 注入 property ...... 当使用组合式 API 时,我们不再使用 this,取而代之的是,插件将在内部利用 provide 和 inject 并暴露一个组合函数。
延续系列的主题,本文将继续尝试立足于相关模块的单元测试解读和适度源码分析,主要考察 Vue 3.x Composition API 中的 provide() 和 inject() 两个方法;希望能在结合阅读文档的基础上,更好地理解相关模块。
我们将要观察三个代码仓库,分别是:
vue
- Vue 2.x 项目@vue/composition-api
- 结合 Vue 2.x “提前尝鲜” Composition API 的过渡性项目vue-next
- Vue 3.x 项目,本文分析的是其 3.0.0-beta.15 版本// composition-api/src/apis/inject.ts
export function provide<T>(
key: InjectionKey<T> | string,
value: T
): void
export function inject<T>(
key: InjectionKey<T> | string
): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T
): T
interface InjectionKey<T> extends Symbol {}
考察 composition-api/test/apis/inject.spec.js
文件:
const Msg = Symbol()
作为 keymsg: inject(Msg)
app.$children[0].msg = 'bar'
的形式赋新值const State = Symbol()
作为 keyprovide( State, reactive({ msg: 'foo' }) )
state: inject(State)
app.$children[0].state.msg = 'bar'
的形式赋新值provide()
以及在 provide Options API 中指定属性简单分析源码,主要函数的调用关系为:
vm._provided
内部对象inject()
只能在 setup()
或 functional component 中使用.value
的;其基本实现如下:// src/setup.ts
function asVmProperty(
vm: ComponentInstance,
propName: string,
propValue: Ref<unknown>
) {
const props = vm.$options.props
if (!(propName in vm) && !(props && hasOwn(props, propName))) {
proxy(vm, propName, {
get: () => propValue.value,
set: (val: unknown) => {
propValue.value = val
},
})
}
}
Vue 3.x beta 中 provide/inject 的签名和之前 @vue/composition-api 中一致,在此不再赘述。
考察文件 packages/runtime-core/__tests__/apiInject.spec.ts
:
const Provider = {
setup() {
provide('foo', 1)
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject('foo')
return () => foo
}
}
const foo = inject('foo', undefined)
且 'foo' 未在 provide() 中注册过的时侯,不应报错// packages/runtime-core/src/apiInject.ts
export function provide<T>(key: InjectionKey<T> | string, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
// 如果自身没有 provides,就直接用父组件的
// 反之,以父组件的 provides 为原型创建自己的
// 这样在 `inject` 中就可以简单地搜索到原型链上所有的了
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
provides[key as string] = value
}
}
而这个 provides 根源上的初始值定义在:
// packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
return {
...
provides: Object.create(null)
}
}
else if
,直接返回明确传入的 undefined:if (key in provides) {
return provides[key as string]
} else if (arguments.length > 1) {
return defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
store 也可以通过全局 API 更改提案中 App 级别的 provide 来提供,但是消费它的组件中的 useStore 风格的 API 还是相同的。
An app instance can also provide dependencies that can be injected by any component inside the app. This is similar to using the provide option in a 2.x root instance.
也给出了一个示例:
// in the entry
app.provide({
[ThemeSymbol]: theme
})
// in a child component
export default {
inject: {
theme: {
from: ThemeSymbol
}
},
template: `<div :style="{ color: theme.textColor }" />`
}
vue-next
中实现了这部分逻辑:
// packages/runtime-core/src/apiCreateApp.ts
interface App<HostElement = any> {
...
provide<T>(key: InjectionKey<T> | string, value: T): this
}
...
const app: App = {
...
provide(key, value) {
if (__DEV__ && key in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
// TypeScript doesn't allow symbols as index type
context.provides[key as string] = value
return app
}
...
}