🚀Vue3 getCurrentInstance以及provide&inject的实现
getCurrentInstance 的实现
getCurrentInstance 可以获取到内部函数的实例
注意:只能在 setup 或生命周期中使用
基于官网描述的特性,我们来试一下实现它。
既然只能在 setup 内部使用,我们自然联想到之前章节的setupStatefulComponent(),它内部调用了instance.type.setup()函数
function setupStatefulComponent(instance: any) {
const Component = instance.type
instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers)
const { setup } = Component
if (setup) {
const setupResult = setup(shallowReadonly(instance.props), {
emit: instance.emit,
})
handleSetupResult(instance, setupResult)
}
finishComponentSetup(instance)
}
为了提升该对象的公用性,我们在全局定义一个currentInstance变量
export let currentInstance = null
// 获取当前实例
export function getCurrentInstance() {
return currentInstance
}
// 设置当前实例
function setCurrentInstance(instance: any) {
currentInstance = instance
}
setCurrentInstance的调用时机决定了currentInstance当前指向哪一个组件实例。
那什么时候设置setCurrentInstance为当前组件实例最好呢?
其实是确保instance.type.setup有值并且在instance.type.setup调用之前这个时机就是最佳时机。
function setupStatefulComponent(instance: any) {
const Component = instance.type
instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers)
const { setup } = Component
if (setup) {
// currentInstance设置为instance
setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), {
emit: instance.emit,
})
handleSetupResult(instance, setupResult)
// 让currentInstance变为null
setCurrentInstance(null)
}
finishComponentSetup(instance)
}
最后我们把currentInstance变为 null,其实是为了使 currentInstance 在 setup 内部有值,这遵循了官方给出的特性。
这就是 getCurrentInstance 的实现。
provide & inject 实现
父组件和子组件之间共享数据我们可以通过很多种方式:
vuexprops和emit- 全局对象
globalProperties - parent & 模板 ref
注意:eventbus 在 vue3 中已不再支持,需要自己手写实现。废除了
$on、$children、$listeners
那如果子组件和祖先组件数据如何共享?props 一层一层的传递?emit 一层一层的暴露方法给父组件?这未免也太不优雅了吧!
provide和inject是这种应用场景的解决方案。
如何实现 provide 和 inject?
下面我们把使用 provide 的组件称为
提供者,把使用 inject 的组件称为接收者。
把提供者实例上的 provide 属性作为一个容器,这个容器就是提供给接收者的共享数据。
接收者如何拿到提供者的共享数据呢?
可以在接收者实例上添加一个 parent 字段,用来指定该组件实例的父组件实例是谁,从而拿到父组件实例的身上的 provide 这个容器。
在创建组件实例的时候为 instance 新增两个属性(provides、parent)
export function createComponentInstance(vnode: any, parentComponent: any) {
const type = vnode.type
const instance = {
vnode,
type,
render: null,
setupState: {},
props: {},
emit: () => {},
slots: {},
provides: {} as Record<string, any>, // 新增
parent: parentComponent, // 新增 父组件的组件实例
}
instance.emit = emit.bind(null, instance) as any
return instance
}
根据官方的描述和例子,provide 接收两个参数name和value,name 用于标识那些提供给子组件的数据,value 就是我们要对外提供的数据。
这里我把instance.provides初始化为一个对象,之所以选择用对象作为容器是因为 provide 具有键值关系。
provide把要提供的数据存储起来,所以大体上provide的实现如下:
export function provide<T>(key: string | number, value: T) {
// 提供者
const currentInstance: any = getCurrentInstance()
if (currentInstance) {
let { provides } = currentInstance
provides[key] = value
}
}
provide已经实现了,那么inject他就是一个从容器中拿取数据的一个过程,不过这个容器要在 parent(父组件)上获取父组件的 provides。
export function inject<T>(key: string, defaultValue?: unknown) {
// 接收者
// 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide
const currentInstance: any = getCurrentInstance()
if (currentInstance) {
const parentProvides = currentInstance.parent.provides
return parentProvides[key]
}
}
此时我在这里准备的 demo 就已经可以使用 provide 和 inject 完成父子组件的数据传参了。
// 提供者
const Provider = {
name: 'Provider',
setup() {
provide('foo', 'fooVal')
provide('bar', 'barVal')
},
render() {
return h('div', {}, [h('p', {}, 'Provider'), h(Consumer)])
},
}
// 接收者
const Consumer = {
name: 'Consumer',
setup() {
const fooVal = inject('foo')
const barVal = inject('bar')
return {
fooVal,
barVal,
}
},
render() {
return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
},
}
当然,不一定是父子组件这种关系这么简单,它可以是爷爷和孙子组件的关系、太爷爷和太孙子组件的关系......,这种情况给你如何处理?比如下面的情况:
我在 Provider 和 Consumer 中间加了一层组件叫ProviderTwo,用来模拟跨组件数据共享这样的情景。我们依然沿用之前的逻辑,发现Consumer的 foo 和 bar 为 undefined。
const Provider = {
name: 'Provider',
setup() {
provide('foo', 'fooVal')
provide('bar', 'barVal')
},
render() {
return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)])
},
}
const ProviderTwo = {
name: 'ProviderTwo',
setup() {},
render() {
return h('div', {}, [h('p', {}, `ProviderTwo`), h(Consumer)])
},
}
const Consumer = {
name: 'Consumer',
setup() {
const fooVal = inject('foo')
const barVal = inject('bar')
return {
fooVal,
barVal,
}
},
render() {
return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
},
}
这是为什么呢?其实 Consumer 的父组件 ProviderTwo 并没有给 provide 属性提供数据,是个空对象。Consumer 在使用 inject 的时候拿了 ProviderTwo 的空对象,结果当然为 undefined。
export function createComponentInstance(vnode: any, parentComponent: any) {
const type = vnode.type
const instance = {
vnode,
type,
render: null,
setupState: {},
props: {},
emit: () => {},
slots: {},
provides: {} as Record<string, any>,
parent: parentComponent, // 父组件的组件实例
}
instance.emit = emit.bind(null, instance) as any
return instance
}
我能想到的就,provides 不再指向空对象,而是指向上一级父组件的 provides,一层一层的指向父组件的 provides,直到没有父组件为止。
我们来改写一下createComponentInstance():
export function createComponentInstance(vnode: any, parentComponent: any) {
const type = vnode.type
const instance = {
vnode,
type,
render: null,
setupState: {},
props: {},
emit: () => {},
slots: {},
provides: parentComponent ? parentComponent.provides : ({} as Record<string, any>), // 确保中间层的组件没有提供provide时,子组件拿最近的有provide的父组件的数据
parent: parentComponent, // 父组件的组件实例
}
instance.emit = emit.bind(null, instance) as any
return instance
}
需求再次升级,这次我们想在 ProviderTwo 组件内使用 provide 和 inject,期望是:ProviderTwo 组件能接收 Provider 的 provide 数据,Consumer 能接收 ProviderTwo 的 provide 数据。
export const Provider = {
name: 'Provider',
setup() {
provide('foo', 'fooVal')
provide('bar', 'barVal')
},
render() {
return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)])
},
}
const ProviderTwo = {
name: 'ProviderTwo',
setup() {
provide('foo', 'fooTwo')
provide('bar', 'barTwo')
// 期望得到provider的foo---fooVal,实际上得到的是fooTwo
const foo = inject('foo')
const bar = inject('bar')
return {
foo,
bar,
}
},
render() {
return h('div', {}, [h('p', {}, `ProviderTwo-${this.foo}-${this.bar}`), h(Consumer)])
},
}
const Consumer = {
name: 'Consumer',
setup() {
const fooVal = inject('foo')
const barVal = inject('bar')
return {
fooVal,
barVal,
}
},
render() {
return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
},
}
我们期望 ProviderTwo 的 foo 和 bar 应该是 Provider 所提供的 fooVal 和 barVal,现实却是 fooTwo 和 barTwo。这是为什么呢?
原因是:在createComponentInstance()的时候 instance 的 provides 是直接指向父组件的 provides,而 ProviderTwo 组件中 provides 被重新赋值为 fooTwo 和 barTwo,又因为 provides 是引用类型,所以它事实上间接改变了父组件的 provides 的值。
举个栗子 🌰:
let father = {
foo: 'fooVal',
}
let obj3 = {
provides: father,
}
obj3.provides['foo'] = 'changed'
console.log(father.foo) // output: changed
那么如何解决这个问题呢?
我们可以用原型链的思想,为当前的组件的 provides 创建一个原型链,原型对象指向父组件的 provides。这样就不必担心对象的引用问题,当前组件的 provides 没有该数据的时候,他会沿着原型链向上寻找该数据,知道找不到为止。如下:我用Object.create创建一个原型对象是 father 的对象。
let father = {
foo: 'fooVal',
}
let obj3 = {
provides: father,
}
obj3.provides = Object.create(father)
obj3.provides['foo'] = 'changed'
console.log(father.foo) // output: fooVal
console.log(obj3.provides['foo']) // output: changed
用这种思想解决 provide 的问题,代码将会是如下:
export function provide<T>(key: string | number, value: T) {
// 提供者
// key和value存在哪呢?挂在instance的provides属性上吧!
const currentInstance: any = getCurrentInstance()
if (currentInstance) {
let { provides } = currentInstance
const parentProvides = currentInstance.parent?.provides
if (provides === parentProvides) {
// 把provide原型指向父组件的provide
provides = currentInstance.provides = Object.create(parentProvides)
}
provides[key] = value
}
}
❗ 里面的判断条件provides === parentProvides是为了避免重复使用 provide 造成组件实例的 provides 被初始化。
如果你还想实现 inject 的默认值功能,代码将会是如下:
export function inject<T>(key: string, defaultValue?: T) {
// 接收者
// 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide
const currentInstance: any = getCurrentInstance()
if (currentInstance) {
const parentProvides = currentInstance.parent.provides
if (key in parentProvides) {
return parentProvides[key]
} else {
// 找不到注入的
// 如果默认值是函数,执行函数
if (isFunction(defaultValue)) {
return defaultValue()
}
return defaultValue
}
}
}
对了,这个默认值功能支持传入一个返回默认值的函数。
用法:
let injectValue = inject('foo', () => 'this is default value')