三分钟学会如何获取vue3setup语法组件ref全部导出

825 阅读2分钟

为什么需要获取全部导出

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生命周期

  1. 组件有生命周期,vnode同时也有生命周期,需要做的就是介入这个对应的生命周期做文章,onVnodeBeforeMount就是生命周期之一。
  2. vnode如果是组件则可以在上面获取component属性,ref进行设置的时候是在patch阶段,所以我们只要在patch直接之前,先一步对挂载导出的数据进行一步拦截,也是就是exposeProxy,为什么需要对这个属性进行先一步处理就可以实现。
  3. 因为vue内部会对这个属性进行缓存判断,如果没有的话,就会进行处理,也就是我们只要先一步处理了之后,vue就会跳过,不再对这个导出进行ref导出的逻辑了。

封装

  1. 如果每一个组件都这样去写肯定不优雅,肯定要优雅起来,起码onVnodeBeforeMount不想每次都写一次,这个时候其实我们可以使用vue提供的compileApi,进行先一步代码转换。
  2. 这里我的实现思路分为两步,如果当前组件是使用的setup语法那么就对html中写入了ref的标签进行添加:onVnodeBeforeMount="onVnodeBeforeMountRef"这句话。
  3. 第一步是编译时。
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();
	}
};
  1. 第二步为运行时,因为需要onVnodeBeforeMountRef的实现,所以我们可以把函数的时间挂载到vue3的globalProperties,这样就不需要进行每次导入。
export default (app: App) => {
app.config.globalProperties.onVnodeBeforeMountRef_ = onVnodeBeforeMountRef_;
};

完整实现,点这里

如果对vue的compileApi感兴趣,之后再开坑,讲@vue/compiler-sfc的实现和转换。