Skyphobia

Vue 组件封装指南(三)——隔代传值(provide & inject)

Front-EndjsVue

Provide & Inject

我们在使用 Element UI / iview 等 UI 框架的表单组件时,往往会根据业务对表单的布局进行一些修改,比如:

HTML
01<el-form ref="form" :model="form" :rules="rules" label-width="120px">
02 <el-form-item prop="username" label="username">
03 <el-input v-model="form.username" />
04 </el-form-item>
05 <el-row :gutter="12">
06 <el-col :span="12">
07 <el-form-item prop="password" label="password">
08 <el-input type="password" v-model="form.password" />
09 </el-form-item>
10 </el-col>
11 <el-col :span="12">
12 <el-button type="primary" @click="handleSubmit"> submit </el-button>
13 </el-col>
14 </el-row>
15</el-form>

表单字段的校验以及 label 宽度的处理显然是在 el-form-item 组件中进行的,而实际上 el-form-item 的父组件确是 el-col……这里就出现了一个令人摸不着头脑的情况——参数从父组件传递到了孙组件。为什么 Element UI 的 el-form 组件可以隔代传值给 el-form-item 组件的 ruleslabel-width 等参数?以及为什么需要用这种方式来实现隔代传值?

依赖注入

Vue 提供了 provideinject 两个对应的选项用于配置依赖注入关系,具体可以参照官方文档。值得一提的,依赖注入的 key 值可以是一个 Symbol,可以有效地避免因为后代 provide 命名重复导致注入出错的问题。

解决无法响应的问题

根据官方文档:

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。

似乎利用依赖注入进行的隔代传值无法实时响应,但这并不意味着我们不能利用某些“特殊手段”来突破这个限制:

然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

Provider.vue

HTML
01<template>
02 <section>
03 <slot />
04 <button @click="$emit('change', 'zh')">
05 English => 简体中文
06 </button>
07 <button @click="$emit('change', 'en')">
08 简体中文 => English
09 </button>
10 </section>
11</template>
12
13<script>
14export default {
15 provide() {
16 return {
17 instance: this, // 将父组件的实例作为依赖
18 };
19 },
20
21 props: {
22 lang: {
23 type: String,
24 default: "",
25 },
26 },
27};
28</script>

Child.vue

HTML
01<template>
02 <section>会响应的 lang: {{ instance.lang }}</section>
03</template>
04
05<script>
06export default {
07 inject: ["instance"],
08};
09</script>

App.vue

HTML
01<template>
02 <I18nProvider :lang="lang" @change="handleChangeLang">
03 lang 的值:{{ lang }}
04 <section>
05 <Child />
06 </section>
07 </I18nProvider>
08</template>
09
10<script>
11import I18nProvider from "./I18nProvider";
12import Child from "./Child";
13
14export default {
15 components: {
16 I18nProvider,
17 Child,
18 },
19
20 data() {
21 return {
22 lang: "en",
23 };
24 },
25
26 methods: {
27 handleChangeLang(lang) {
28 this.lang = lang;
29 },
30 },
31};
32</script>

这里展示的正是 Element UI 中 el-form 组件的 hack 手段,直接将 Provider 组件的实例作为依赖注入到子组件,通过实例本身的 setter 和 getter 来解决子组件注入的依赖无法响应的问题。

原理

provideinject 的实现比较简单,Vue 源码的 inject.js 文件中能找到 resolveInject 函数,这歌函数中包含了处理注入的核心实现。 子组件会根据 inject 层层遍历 $parent,直到找到 provide 为止。

单这样看使用依赖注入造成的遍历开销还是会对性能造成些许影响,加之依赖注入本身存在隐式依赖的问题,业务代码中最好限制使用范围,不应该再进一步将一个难以追踪的数据扩散到组件中去。

应用场景初探

利用隔代传值的方便之处在于共用同一套状态的子组件不再需要将状态从父组件一一传递到目标组件,因此很适合一些封装组件的开发场景。比如通过一个可以提供子组件 color schema 的 Provider 组件来构建一个带主题的 Tag 组件:

ColoredTag.vue

HTML
01<template>
02 <Child>
03 <slot />
04 </Child>
05</template>
06
07<script>
08import Child from "./Child";
09
10export default {
11 components: {
12 Child,
13 },
14 provide() {
15 return {
16 color: "gold",
17 };
18 },
19};
20</script>

Child.vue

HTML
01<template>
02 <el-tag :color="color">
03 <slot />
04 </el-tag>
05</template>
06
07<script>
08export default {
09 inject: ["color"],
10};
11</script>

这样那样地操作一番,我们就简单地封装出了一个金色的 TAG 组件。但这其实并不怎么实用。

有一个看上去比这更好的使用场景,就是利用依赖注入的特性在根组件创建全局配置注入组件,并在有需要使用到的时候为子组件配置对应的 inject。配合上述响应式的 hack 方式可以实现包括但不限于 i18n、全局主题切换等功能。

杂项