为什么需要获取全部导出
1.expose导出语法设计很好,但是setup组件没有主动导出是没办法获取其他私有属性,但是有些情况难免需要进行获取。
- 直接上代码这是使用setup语法的foo组件
<template>
<div>foo组件</div>
</template>
<script lang="ts" setup>
const a = 5;
const b = 6;
defineExpose({
c:8
})
</script>
正常情况我们对这个组件使用ref,只能获取c这个属性,但是同时还要获取a,b,还要兼容其他写法的话,其实很简单,直接上实现方式,然后接下来进行原理讲解。
<template>
<div>
<Foo ref="foo" :onVnodeBeforeMount="onVnodeBeforeMountRef"></Foo>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, VNode, ComponentInternalInstance } from "vue";
import {onVnodeBeforeMountRef} from "./index";
const foo = ref();
onMounted(() => {
console.log("foo.a", foo.value.a);
console.log("foo.b", foo.value.b);
});
</script>
index.ts
const hasOwn = (val: Record<string, any>, key: string) =>
Object.prototype.hasOwnProperty.call(val, key);
const onVnodeBeforeMountRef = (vNode: VNode) => {
const component = vNode.component! as ComponentInternalInstance & {
setupState: Record<string, any>;
};
if (component) {
component.exposeProxy = new Proxy(
{},
{
get(_, key: string) {
//主动导出的属性都在exposed上,我们可以优先判断获取的属性是否是在导出里面
if (component.exposed && hasOwn(component.exposed, key))
return component.exposed[key];
//然后在判断是不是setup函数中返回的属性,setup返回的都可以在这里找到
if (hasOwn(component.setupState, key))
return component.setupState[key];
//如果不是以上,比如props之类的,就交给vue原本的获取逻辑上进行获取
//@ts-ignore
return component.proxy[key];
},
}
);
}
};
vnode生命周期
- 组件有生命周期,vnode同时也有生命周期,需要做的就是介入这个对应的生命周期做文章,onVnodeBeforeMount就是生命周期之一。
- vnode如果是组件则可以在上面获取component属性,ref进行设置的时候是在patch阶段,所以我们只要在patch直接之前,先一步对挂载导出的数据进行一步拦截,也是就是exposeProxy,为什么需要对这个属性进行先一步处理就可以实现。
- 因为vue内部会对这个属性进行缓存判断,如果没有的话,就会进行处理,也就是我们只要先一步处理了之后,vue就会跳过,不再对这个导出进行ref导出的逻辑了。
封装
- 如果每一个组件都这样去写肯定不优雅,肯定要优雅起来,起码onVnodeBeforeMount不想每次都写一次,这个时候其实我们可以使用vue提供的compileApi,进行先一步代码转换。
- 这里我的实现思路分为两步,如果当前组件是使用的setup语法那么就对html中写入了ref的标签进行添加:onVnodeBeforeMount="onVnodeBeforeMountRef"这句话。
- 第一步是编译时。
const transform = (source: string, filename: string) => {
const { descriptor } = parse(source);
if (descriptor.template) {
const { loc: templateLocStart } = descriptor.template;
const magicString = new MagicString(source);
const onRefNode: NodeTransform = (node) => {
if (node.type === 1 /** NodeTypes.ELEMENT */) {
const ref = findProp(node, 'ref');
if (ref && ref.type === 6 /** NodeTypes.ATTRIBUTE */) {
const { loc } = ref;
magicString.appendLeft(
loc.end.offset + templateLocStart.start.offset,
onVnodeBeforeMountRef
);
}
}
};
compileTemplate({
filename,
source: descriptor.template.content,
id: filename,
compilerOptions: {
nodeTransforms: [onRefNode]
}
});
return magicString.toString();
}
};
- 第二步为运行时,因为需要onVnodeBeforeMountRef的实现,所以我们可以把函数的时间挂载到vue3的globalProperties,这样就不需要进行每次导入。
export default (app: App) => {
app.config.globalProperties.onVnodeBeforeMountRef_ = onVnodeBeforeMountRef_;
};
完整实现,点这里
如果对vue的compileApi感兴趣,之后再开坑,讲@vue/compiler-sfc的实现和转换。