getCurrentInstance
这也是Vue3暴露给用户的一个api,它只能在setup中使用,返回当前组件的instance。我们分别在App和Foo组件的setup中调用。
export const App = {
name: 'App',
render() {
return h('div', {}, [h('p', {}, 'currentInstance demo'), h(Foo)]);
},
setup() {
const instance = getCurrentInstance();
console.log('App:', instance);
}
};
export const Foo = {
name: 'Foo',
setup() {
const instance = getCurrentInstance();
console.log('Foo:', instance);
return {};
},
render() {
return h('div', {}, 'foo');
}
};
如何在组件中拿到自己的instance呢,getCurrentInstance不仅不用传参,还限定在setup中调用。考虑使用全局变量currentInstance,在调用setup之前,设置该变量的值。getCurrentInstance只需将其返回即可。
export function getCurrentInstance() {
return currentInstance;
}
function setCurrentInstance(instance) {
currentInstance = instance;
}
function setupStatefulComponent(instance: any) {
const Component = instance.type;
instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);
const { setup } = Component;
if (setup) {
// 设置currentInstance,然后再调用setup
setCurrentInstance(instance);
const setupResult = setup(shallowReadonly(instance.props), {
emit: instance.emit
});
setCurrentInstance(null);
handleSetupResult(instance, setupResult);
}
}
实现完毕,现在可以在控制台看到APP和Foo的instance:
provide / inject
父组件使用provide传值,子组件使用inject收到值,中间可以跨越很多层,并且provide / inject只能在setup中调用。 有以下难点需要克服:
- 调用provide后,这些键值对应该存放在哪里,假设它们称为provides。
- 子组件如何访问父组件的provides,同时子组件provide相同key时,不改变父组件的provides。
- inject可以传入第二个参数表示默认值,默认值还可以是函数,将拿出它的返回值。
经过之前的学习,不难想象应该给instance添加provides属性。
provide的最简实现:
export function provide(key, value) {
// 因为用到了getCurrentInstance,所以provide/inject必须在setup中使用
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides } = currentInstance;
provides[key] = value;
}
}
那么inject的逻辑就应该是,获取父结点的instance,从中取出provides,再用key访问对应的属性。问题来了,怎么获取父结点的instance?
给instance添加属性parent,表示父结点的instance,parent作为第二个参数传给createComponentInstance。随着函数签名的改变,很多函数都需要添加参数parentComponent。
最后到了函数setupRenderEffect中,将instance传入patch,作为parentComponent:
function setupRenderEffect(instance: any, initialVNode, container) {
const { proxy } = instance;
const subTree = instance.render.call(proxy);
// 此处添加第三个参数
patch(subTree, container, instance);
initialVNode.el = subTree.el;
}
为什么这里传入的instance就是父结点的instance?可以稍微分析一下:
假设Foo是App的子组件,并且App没有其他children:
render -> patch(instance === undefined) -> 处理App的component vnode(生成App的instance) -> patch -> 处理App的element vnode -> mountChildren -> patch(传入App的instance) -> 处理Foo的component vnode(生成Foo的instance,并且拿到父结点的instance) -> patch -> 处理Foo的element vnode -> Foo挂载完毕 -> App挂载完毕
现在的provide/inject已经可以处理相邻父子的传值了,但是爷孙及以上会失败。因为孙只能访问父的provides,访问不到爷上的。
修改provides的初始化方式,父的provides指向爷的provides,子的provides指向父的provides。
export function createComponentInstance(vnode, parent) {
const component = {
vnode,
type: vnode.type,
setupState: {},
props: {},
slots: {},
// 这里
provides: parent ? parent.provides : {},
parent,
emit: () => {}
};
但是这样也有问题,因为引用类型,如果父结点使用provide提供了和爷结点相同key的属性,就会修改自己instance的provides,进而修改了爷结点的provides。所以使用原型链,currentInstance.provides = Object.create(parent.provides)
。是不是有寄生组合继承内味了?先在当前instance.provides找,找不到就沿着原型链向上找,并且规避了引用类型的影响,修改当前provides也不影响parent。
export function provide(key, value) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides, parent } = currentInstance;
const parentProvides = parent.provides;
// 可能调用多次provide,但只用初始化一次
// 根据上一个代码块,可知一开始 provides === parentProvides
if (provides === parentProvides) {
// 初始化
provides = currentInstance.provides = Object.create(parentProvides);
}
// 后来因为这里修改了,全等关系就被破坏了
provides[key] = value;
}
}
最后完成inject可提供默认值的需求,比较简单:
export function inject(key, defaultValue) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
const parentProvides = currentInstance.parent.provides;
if (key in parentProvides) {
return parentProvides[key];
} else if (defaultValue) {
// 默认值类型是函数,就拿返回值
if (typeof defaultValue === 'function') {
return defaultValue();
}
return defaultValue;
}
}
}
组件:
// 组件 provide 和 inject 功能
import { h, provide, inject } from '../../lib/my-mini-vue.esm.js';
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');
const foo = inject('foo');
return {
foo
};
},
render() {
return h('div', {}, [
h('p', {}, `ProviderTwo foo:${this.foo}`),
h(Consumer)
]);
}
};
const Consumer = {
name: 'Consumer',
setup() {
const foo = inject('foo');
const bar = inject('bar');
// const baz = inject('baz', 'bazDefault');
const baz = inject('baz', () => 'bazDefault');
return {
foo,
bar,
baz
};
},
render() {
return h('div', {}, `Consumer: - ${this.foo} - ${this.bar}-${this.baz}`);
}
};
export default {
name: 'App',
setup() {},
render() {
return h('div', {}, [h('p', {}, 'apiInject'), h(Provider)]);
}
};