element-plus 中向子组件传递数据的方案——provide/inject 与 symbol 类型的组合使用
公司前端组件库用的是 element UI 旧版本改造的,有天无聊在看组件库 issues 的时候发现了一个有意思的:
“dialog 的 z-index 默认是 2000,有些场景下需要更高/更低的 zIndex,能不能加一个 feature?”
组件负责人也觉得很有业务场景,遂根据 element-plus 中的 config-provider 做了一组件,只需要向 config-provider 传入需要 z-index 初始值就行。
很好奇是怎么实现的,于是去下了 elementPlus 的源码来看。
实现原理
我猜想,基于改动比较小、应用范围广的考虑,他们选择在原有组件之上加了一个父组件——config-provider,在 config-provider 中通过 provide/inject 向下传递从 props 中获取到的数据,子组件中应该只需要引用一个通用的 hook 就能完成获取目标属性的逻辑。
代码中确实也是这样设计的
首先,需要明白 config-provider 这一层实际上是不需要有具体内容的,只是作为一个接受数据传递数据的中间容器,所以不渲染具体内容直接使用renderSlot渲染子组件。
import { defineComponent, renderSlot, watch } from 'vue'
import { provideGlobalConfig } from './hooks/use-global-config'
import { configProviderProps } from './config-provider-props'
......
setup(props, { slots }) {
watch(
() => props.message,
(val) => {
Object.assign(messageConfig, val ?? {})
},
{ immediate: true, deep: true }
)
const config = provideGlobalConfig(props) // 这里
return () => renderSlot(slots, 'default', { config: config?.value })
},
provideGlobalConfig 执行了向下传递数据的逻辑
// provideGlobalConfig
import {
......
zIndexContextKey,
} from '@element-plus/hooks'
......
const provideFn = app?.provide ?? (inSetup ? provide : undefined)
provideFn(
zIndexContextKey, // 这里
computed(() => context.value.zIndex)
)
......
return context
最终在 dialog 组件中获取 zIndex
// useDialog
import {
......
useZIndex,
} from '@element-plus/hooks'
const { nextZIndex } = useZIndex() // 这里
const zIndex = ref(props.zIndex ?? nextZIndex())
return{
......
zIndex
}
为什么要用 symbol?——symbol 类型的浅析
原因:
从一开始,我就有一个疑问,为什么在使用 provide()传递数据时 InjectionKey 要用 symbol 而不是任意的一个字符串
function provide<T>(key: InjectionKey<T> | string, value: T): void
看到一个比较可靠的解释说是为了避免别人知道 InjectionKey 具体值之后胡乱注入导致组件运行异常,而 symbol 类型的唯一性恰恰好能够解决这个问题,就算知道 InjectionKey ,重新生成的 symbol 也是不一样的!
上文中 provideGlobalConfig 里使用的 zIndexContextKey:
export const zIndexContextKey: InjectionKey<Ref<number | undefined>> =
Symbol("zIndexContextKey");
而 dialog 中使用的 useZIndex 也同样使用了这个 zIndexContextKey
const zIndexInjection =
zIndexOverrides ||
(getCurrentInstance() ? inject(zIndexContextKey, undefined) : undefined);
symbol 浅析
symbol 类型的作用其实很简单,就是为了保证在当前环境中,有且只有一个值;如果希望你定义的类里面某个方法不会被其他使用者的同名方法覆盖修改,使用 symbol 就能定义个唯一的方法名。
symbol 类型变量生成
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2; // false
// 有参数的情况
let s1 = Symbol("foo");
let s2 = Symbol("foo");
s1 === s2; // false
一些使用特点
-
Symbol 值不能与其他类型的值进行运算,会报错。
let sym = Symbol("My symbol"); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}`; // TypeError: can't convert symbol to string
-
symbol 可以显式转为字符串和布尔值
// string let sym = Symbol("My symbol"); String(sym); // 'Symbol(My symbol)' sym.toString(); // 'Symbol(My symbol)' // boolean let sym = Symbol(); Boolean(sym); // true !sym; // false if (sym) { // ... }
为什么引用的 symbol 是同一个?——ES6 模块引入和CommonJS的差异
ES6 模块跟 CommonJS 模块的不同,主要有以下两个方面:
- ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
- ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载
之前总是认为不同文件 import 加载同一个变量时,实际得到的是两个不同的东西,其实是同一个变量的引用,而 CommonJS 获取的才是原始值的拷贝
同时应该注意第二点,执行时机的不同,ES6 执行会导致有以下两个特点:
- import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
- export 命令会有变量声明提前的效果。
深入理解 ES6 模块机制: https://zhuanlan.zhihu.com/p/33843378