element-plus 中向子组件传递数据的方案——provide/inject 与 symbol 类型的组合使用

1 篇文章 0 订阅
1 篇文章 0 订阅

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
一些使用特点
  1. 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
    
  2. 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 模块的不同,主要有以下两个方面:

  1. ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
  2. ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载

之前总是认为不同文件 import 加载同一个变量时,实际得到的是两个不同的东西,其实是同一个变量的引用,而 CommonJS 获取的才是原始值的拷贝

同时应该注意第二点,执行时机的不同,ES6 执行会导致有以下两个特点:

  1. import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
  2. export 命令会有变量声明提前的效果。

深入理解 ES6 模块机制: https://zhuanlan.zhihu.com/p/33843378

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue中,provideinject是一对用于在父组件和子组件之间传递数据的API。但是,当我们在provide中提供响应式数据时,它并不会自动触发子组件的更新。 为了实现响应式数据更新,我们可以使用Vue中的$forceUpdate()方法。具体来说,我们可以在provide中提供一个包含响应式数据的对象,并在子组件使用inject将其注入。然后,在父组件中更新数据时,我们可以在更新数据后手动调用$forceUpdate()方法强制更新子组件。 以下是一个示例代码: 父组件: ``` <template> <div> <ChildComponent /> </div> </template> <script> import ChildComponent from './ChildComponent.vue'; export default { provide() { return { data: this.data } }, data() { return { data: {} } }, methods: { updateData() { this.data.name = 'new name'; this.$forceUpdate(); } }, components: { ChildComponent } } </script> ``` 子组件: ``` <template> <div> {{data.name}} </div> </template> <script> export default { inject: ['data'], created() { console.log(this.data.name); // 输出空字符串 }, mounted() { console.log(this.data.name); // 输出空字符串 }, updated() { console.log(this.data.name); // 输出'new name' } } </script> ``` 在这个例子中,父组件提供了一个包含响应式数据的data对象,并将其注入到子组件中。在父组件中更新数据时,我们手动调用了$forceUpdate()方法,从而强制更新了子组件的视图。在子组件中,我们可以通过inject注入父组件提供的data对象,并在updated钩子函数中获取更新后的数据。 需要注意的是,$forceUpdate()方法会触发所有子组件的更新,因此使用时需要谨慎。如果可能的话,我们应该尽量避免使用这个方法,而是通过Vuex或事件总线等其他方式来进行组件间的通信。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值