Vuetify v3(1)- defineComponent
前言
我个人非常喜欢谷歌的 MD 设计,Vue
组件库这块 vuetify
是该设计的杠把子
尤其是升级到对应 Vue3
的 v3
版本之后,其内部实现让人眼前一亮
大概是因为 Vue2
为数不多的逻辑复用方式 mixin
很容易让人眼前一黑,一对比下来就显得新版的 vuetify
很牛逼
属性透传
回到重点,在设计组件库时,嵌套组件的属性透传、默认值复写等一直让人头疼
vuetify
提供了一种较为优雅的解决方法,每个组件都是由二次封装的 defineComponent
方法声明的,例如 v-btn
的源码:
1 | import { defineComponent, ... } from '@/util' |
该方法构建了一个数据流,该数据流从组件树顶部传到各个叶子节点
只要上层节点提供了需要复写的参数,下层子组件只要在该数据流中(通过 defineComponent
声明),就必定会被复写参数,子组件无需主动去获取
数据不仅能够传递到嵌套多层的组件中,还能让其只在局部生效,非常灵活
实现
这玩意相当于一个中间件,接管了部分参数的 inject
, provide
行为,复写了传参中的 setup
函数,总体实现可以总结成以下步骤:
从约定的
default
命名空间中inject
参数哈希表使用
setup
的外部传参props
构建一个新的响应式_props
根据当前组件名在哈希表中获得该组件需要覆盖的参数,合并后传给原本的
setup
函数
具体实现要复杂一些
处理组件声明中的 setup 和 props
首先将原本的
setup
记为_setup
1
options._setup = options._setup ?? options.setup;
后续需要通过组件名来注入默认属性,所以检查下有无组件名
1
2
3
4
5
6if (!options.name) {
consoleWarn(
'The component is missing an explicit name, unable to generate default prop value'
);
return options;
}格式化
props
1
2
3
4
5
6options.props = options.props ?? {};
// propsFactory 是一个工具方法,便于定义选项式 props
// 这里相当于格式化了以下,不用在意
options.props = propsFactory(options.props, toKebabCase(options.name))();
// 手动加入 _as 定义
options.props._as = String;其中还添加了
_as
属性的声明当一个
v-btn
组件需要使用v-tab
的默认属性时,可以在该组件上标记_as="VTab"
复写 setup
1 | options.setup = function setup (props: Record<string, any>, ctx) { |
最核心的实现,简单说就是如下几步:
将
props
变为响应式记住子组件默认值
在响应式的
_props
上更新属性值用响应式的
_props
执行原本的_setup
将子组件的默认值传递(
provide
)到子组件,在子组件默认值发生变化时也会更新传递下去的值返回执行原本
_setup
的返回值
详细说明:
初始化一些变量
1
2
3
4
5
6
7
8// 当前组件实例
const vm = getCurrentInstance()!;
// 注入父组件提供的默认值,内部实现是 inject
const defaults = useDefaults();
// 用于存放子组件需要覆盖的默认值
const _subcomponentDefaults = shallowRef();
// 用于存放变为响应式的初始参数
const _props = shallowReactive({ ...toRaw(props) });记录子组件需要覆盖的默认值,为当前组件覆盖默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32watchEffect(() => {
// 全局的需要覆盖的默认值
const globalDefaults = defaults.value.global;
// 根据组件名拿到当前组件需要覆盖的默认值
const componentDefaults = defaults.value[props._as ?? options.name!];
// 如果需要覆盖
if (componentDefaults) {
// 记录子组件需要覆盖的默认值
const subComponents = Object.entries(componentDefaults).filter(([key]) =>
key.startsWith(key[0].toUpperCase())
);
if (subComponents.length)
_subcomponentDefaults.value = Object.fromEntries(subComponents);
}
// 遍历所有 prop
for (const prop of Object.keys(props)) {
// 记录新值
let newVal = props[prop];
/* 如果外部没有传入该参数,则优先使用需要覆盖的值
* PS: 外部传入了的参数会在 vnode 的 props 中出现
*/
if (!propIsDefined(vm.vnode, prop)) {
newVal =
componentDefaults?.[prop] ?? globalDefaults?.[prop] ?? props[prop];
}
// 更新响应式的属性
if (_props[prop] !== newVal) {
_props[prop] = newVal;
}
}
});上面实现在
watchEffect
中,里面需要覆盖的默认值变动后会自动执行以更新值执行原有的
_setup
,并记录返回值setupBindings
1
const setupBindings = options._setup(_props, ctx);
当子组件需要覆盖的数据变化时,需要更新提供给子组件的覆盖数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let scope: EffectScope;
watch(
_subcomponentDefaults,
(val, oldVal) => {
// 如果不再需要覆盖了(为空),停止更新数据的行为
if (!val && scope) scope.stop();
// 如果是第一次声明需要覆盖的值
else if (val && !oldVal) {
// 上层更新时更新 provide 以将新的数据传到子节点
scope = effectScope();
scope.run(() => {
provideDefaults(
mergeDeep(injectSelf(DefaultsSymbol)?.value ?? {}, val)
);
});
}
},
{ immediate: true }
);最后返回
_setup
的返回值1
return setupBindings;