实现 provide-inject 功能
作用
provide和inject用于在组件树中传递数据,避免通过 props 层层传递provide在祖先组件中提供数据,inject在后代组件中注入数据- 常用于插件开发或跨层级组件通信
实现步骤
- 初始化 App.js
js
// App.js
import { h, provide, inject } from "../lib/guide-mini-vue.esm.js";
const Provider = {
name: "Provider",
render() {
return h("div", {}, [h("p", {}, "Provider"), h(Consumer)]);
},
setup() {
provide("foo", "fooVal");
provide("bar", "barVal");
},
};
const Consumer = {
name: "Consumer",
render() {
return h("div", {}, `Consumer: - ${this.foo} - ${this.bar}`);
},
setup() {
const foo = inject("foo");
const bar = inject("bar");
return {
foo,
bar,
};
},
};
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]);
},
};
- 先实现一个简单的 provide-inject 功能, 存和取,数据放在组件实例上
js
// /runtime-core/apiInject.js ✅
import { getCurrentInstance } from "./component";
export function provide(key, value) {
// 存
const currentInstance:any = getCurrentInstance() // 这个方法仅在 setup 函数中可以使用,所以 provide、inject 也只能在 setup 函数中使用
if(currentInstance) {
const { provides } = currentInstance
provides[key] = value
}
}
export function inject(key) {
// 取
const currentInstance:any = getCurrentInstance()
if(currentInstance) {
const parentProvides = currentInstance.parent.provides
return parentProvides[key]
}
}
// runtime-core/component.ts
export function createComponentInstance(vnode, parent) {
// ✅ parent 用来传递父组件实例 往上回溯,renderer.ts 中好多十几个位置需要加上 parent 参数,形参名为 parentComponent, 到源头 patch(subTree, container, instance)
console.log("createComponentInstance", parent);
const component = {
vnode,
type: vnode.type,
setupState: {},
props: {},
emit:()=>{},
slots:{},
provides: {}, // ✅ 用来存储 provide 的数据
parent // ✅ 记录父组件实例
}
component.emit = emit.bind(null, component) as any // 这里 null 是this指向, component 作为第一个参数传入
return component
}
// renderer.ts
`因为上面的函数 createComponentInstance 需要 parent 参数,所以这里也要传入 parentComponent,而且这个页面涉及的地方比较多,全部改成传入 parentComponent 参数`
// runtime-core/index.ts
export { provide, inject } from './apiInject'
- 结果:我们实现了最简单的 provide-inject 功能,父组件通过 provide 存储数据,子组件通过 inject 获取数据
- 思想:我们开发项目可以按照先实现一个简单的需求的版本,然后再慢慢完善它,属于小步走开发思想,防止我们过度设计,而且实现起来也没有压力。
- 我们在上面已经实现了一个父子组件之间的传递通过 provide-inject 功能,接下来我们实现祖孙组件之间的传递
- 给 App.js 的 Provider 组件和 Consumer 组件中间再加一层组件 Middle---> ProviderTwo 这个组件
js
// App.js
const ProviderTwo = {
name: 'ProviderTwo',
render() {
return h('div', {}, [h('p', {}, "ProviderTwo"), h(Consumer)])
},
setup() {
}
}
- 对相关的祖孙传递逻辑进行实现
-
问题?我们加上一层以后就无法在 App.js 的 Consumer 组件中获取到 Provider 组件中 provide 的数据了
- 分析?因为我们现在的数据是存储在组件实例的 provides 对象上,而 Middle 组件实例的 provides 对象是空的,所以无法获取到 Provider 组件中 provide 的数据
- 解决方案?
- 让实例上的 provides 属性指向父组件实例的 provides 属性,这样就可以实现祖孙组件之间的传递了,代码如下:
js// runtime-core/component.ts export function createComponentInstance(vnode, parent) { console.log("createComponentInstance", parent); const component = { vnode, type: vnode.type, setupState: {}, props: {}, emit:()=>{}, slots:{}, provides: parent ? parent.provides : {}, // ✅ 让实例上的 provides 属性指向父组件实例的 provides 属性 parent } component.emit = emit.bind(null, component) as any // 这里 null 是this指向, component 作为第一个参数传入 return component }- 结果:我们实现了祖孙组件之间的传递
-
我们继续实现更复杂一点的场景,我们在 Middle 组件中也使用 provide 提供一个
provide("foo", "fooTwo"),然后在 Consumer 组件中通过 inject 获取foo的值,看看会获取到哪个值js// App.js const ProviderTwo = { name: 'ProviderTwo', render() { return h('div', {}, [h('p', {}, "ProviderTwo " + this.foo), h(Consumer)]) }, setup() { provide("foo", "fooTwo") const foo = inject("foo") return { foo } } }- 结果:我们会获取到 Middle 组件中提供的
foo的值fooTwo,说明子组件提供的数据会覆盖父组件提供的数据- 还有一个问题,当我们在 Middle 组件中通过 inject 获取
foo的值时,会获取到哪个值呢? - 结果:我们会获取到 Middle 组件中提供的
foo的值fooTwo, 我们应该获取到 Provider 组件中提供的foo的值fooVal,这就不对了
- 还有一个问题,当我们在 Middle 组件中通过 inject 获取
- 分析:因为我们在 Middle 组件中调用 provide 的时候,是把数据存储在 Middle 组件实例的 provides 对象上,而 Middle 组件实例的 provides 对象是继承自 Provider 组件实例的 provides 对象的,所以当我们在 Consumer 组件中通过 inject 获取
foo的值时,会先从 Middle 组件实例的 provides 对象上获取,没有再去祖先组件 Provider 组件实例的 provides 对象上获取- 而我们在 Middle 组件中通过 inject 获取
foo的值时,已经被 Middle 组件实例的 provides 对象上的foo覆盖了,所以获取到的是fooTwo
- 而我们在 Middle 组件中通过 inject 获取
- 解决方案?
jsexport function provide(key, value) { // 存 const currentInstance:any = getCurrentInstance() // 这个方法仅在 setup 函数中可以使用,所以 provide、inject 也只能在 setup 函数中使用 if(currentInstance) { let { provides } = currentInstance const parentProvides = currentInstance.parent.provides if(provides === parentProvides) { provides = currentInstance.provides = Object.create(parentProvides) // ✅ 这里我们需要改造一下 } provides[key] = value } } // 效果:实现了祖先组件 provide, 父亲组件也同时 provide, 子组件 inject,拿到父组件里面的对象里面改过的属性,父组件 inject,拿到是祖先组件的 provides provides: {foo: "fooTwo"} [[Prototype]]:Object bar:"barVal" foo:"fooVal"- 结果:我们实现了祖孙组件之间的传递,并且解决了子组件提供的数据覆盖父组件提供的数据的问题
- 回顾:我们 provide 再次存储值,修改了 provides 对象,让 provides 对象原型指向父组件实例的 provides 对象,这样就实现了数据的继承,同时避免了子组件提供的数据覆盖父组件提供的数据的问题。
- 结果:我们会获取到 Middle 组件中提供的
-
- 我们已经实现了祖孙传值的功能,我们再复杂一点,我们在中间组件 Middle 中
inject注入baz,给一个默认值defaultVal,在页面访问,会展示什么呢?
js
const ProviderTwo = {
name: 'ProviderTwo',
render() {
return h('div', {}, [h('p', {}, "ProviderTwo " + this.foo + this.baz), h(Consumer)]) // ✅ 显示 baz
},
setup() {
provide("foo", "fooTwo")
const foo = inject("foo")
const baz = inject("baz", "defaultVal") // ✅ 给一个默认值
return {
foo,
baz
}
}
}
- 结果:我们会获取到 undefined
- 分析:因为我们在 Middle 组件中通过 inject 获取
baz的值时,发现没有提供baz,所以会返回 undefined,而不是默认值defaultVal - 解决方案?
js
export function inject(key, defaultVal?) {
// 取
const currentInstance:any = getCurrentInstance()
if(currentInstance) {
const parentProvides = currentInstance.parent.provides
if(key in parentProvides) {
return parentProvides[key]
} else if(defaultVal) {
return defaultVal
}
}
}
- 结果:我们实现了在中间组件中注入一个不存在的值时,返回默认值的功能