各位老铁,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里一个特别有意思的家伙:provide/inject
。
这玩意儿,说白了,就是 Vue 里面的“隔空传功”,让组件之间可以跨层级传递数据,而不用一层层地 props
传下去。想象一下,你爷爷想给孙子发个红包,不用先给爸爸,再给儿子,直接微信转账,就这么痛快!
一、provide/inject
:解决什么问题?
在复杂的 Vue 应用中,组件嵌套层级很深是很常见的。如果父组件需要传递数据给很深层的子组件,传统的做法是通过 props
一层层传递。这种方式有两个问题:
- 代码冗余: 中间组件可能并不需要这些数据,但为了传递下去,不得不声明
props
,增加了代码的噪声。 - 维护困难: 如果数据来源发生变化,需要修改所有中间组件的
props
定义,维护成本很高。
provide/inject
就是来解决这些问题的。它允许祖先组件“提供”(provide
)数据,后代组件“注入”(inject
)数据,而不需要中间组件参与。
二、provide/inject
的基本用法
先看个简单的例子:
// Grandfather.vue (祖父组件)
<template>
<div>
<p>我是爷爷</p>
<Child />
</div>
</template>
<script>
import { provide } from 'vue';
import Child from './Child.vue';
export default {
components: {
Child,
},
setup() {
provide('message', '爷爷的爱'); // 提供一个名为 'message' 的数据
provide('age', 70); // 提供一个名为 'age' 的数据
return {};
},
};
</script>
// Child.vue (孙子组件)
<template>
<div>
<p>我是孙子,收到了:{{ message }},爷爷今年{{ age }}岁。</p>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const message = inject('message'); // 注入名为 'message' 的数据
const age = inject('age'); // 注入名为 'age' 的数据
return {
message,
age,
};
},
};
</script>
在这个例子中,Grandfather.vue
组件通过 provide
提供了 message
和 age
两个数据。Child.vue
组件通过 inject
注入了这两个数据,并直接使用。中间的父组件根本不需要知道这些数据的存在。
三、provide
的多种用法
provide
可以接收两种类型的参数:
- 字符串 key: 上面的例子就是用的字符串 key。
- Symbol key: 使用 Symbol 可以避免命名冲突。
// 使用 Symbol key
import { provide, ref, computed, reactive, toRefs } from 'vue';
const messageKey = Symbol('message');
const ageKey = Symbol('age');
const userKey = Symbol('user');
export default {
setup() {
const message = ref('爷爷的爱');
const age = ref(70);
const user = reactive({ name: '老王', gender: '男' });
provide(messageKey, message);
provide(ageKey, age);
provide(userKey, toRefs(user)); // 提供响应式对象
return {};
},
};
四、inject
的多种用法
inject
方法可以接收三个参数:
- key: 必须,用于指定要注入的数据的 key。
- defaultValue: 可选,如果祖先组件没有提供对应 key 的数据,则使用该默认值。
- isFactory: 可选,一个布尔值,表明
defaultValue
是否为一个工厂函数。如果isFactory
为true
,则defaultValue
会被视为一个函数,并被调用来生成默认值。
// inject 示例
import { inject } from 'vue';
export default {
setup() {
const message = inject('message', '默认消息'); // 提供默认值
const age = inject('age', () => 18, true); // 提供工厂函数作为默认值
return {
message,
age,
};
},
};
五、响应式数据的 provide/inject
provide/inject
不仅可以传递静态数据,还可以传递响应式数据。这意味着,如果祖先组件提供的数据发生变化,后代组件也会自动更新。
// Grandfather.vue
<template>
<div>
<p>我是爷爷,今年 {{ age }} 岁</p>
<button @click="increaseAge">长一岁</button>
<Child />
</div>
</template>
<script>
import { provide, ref } from 'vue';
import Child from './Child.vue';
export default {
components: {
Child,
},
setup() {
const age = ref(70);
provide('age', age); // 提供响应式数据
const increaseAge = () => {
age.value++;
};
return {
increaseAge,
};
},
};
</script>
// Child.vue
<template>
<div>
<p>我是孙子,爷爷今年 {{ age }} 岁。</p>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const age = inject('age'); // 注入响应式数据
return {
age,
};
},
};
</script>
在这个例子中,age
是一个响应式数据。当 Grandfather.vue
组件中的 age
发生变化时,Child.vue
组件也会自动更新。
六、源码分析:provide/inject
的实现原理
现在,咱们来扒一扒 provide/inject
的源码,看看它是怎么实现的。
1. provide
的实现
在 Vue 3 中,provide
的实现主要在 packages/runtime-core/src/apiInject.ts
文件中。
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`);
}
return;
}
let provides = currentInstance.provides;
// by default an instance inherits its parent's provides object
// unless the instance is a root node which returns `null`
if (provides === parentProvides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
// TS doesn't allow symbol as index type
provides[key as string] = value;
}
这段代码的核心逻辑如下:
- 首先,判断当前是否在
setup
函数中。provide
只能在setup
函数中使用。 - 获取当前组件实例的
provides
对象。每个组件实例都有一个provides
对象,用于存储它提供的数据。 - 如果
provides
对象和父组件的provides
对象相同,说明当前组件还没有提供任何数据。此时,创建一个新的provides
对象,并将其原型指向父组件的provides
对象。这样做的好处是,如果当前组件没有提供某个 key 的数据,可以从父组件的provides
对象中查找。这就是原型链查找。 - 将 key 和 value 存储到
provides
对象中。
2. inject
的实现
inject
的实现也在 packages/runtime-core/src/apiInject.ts
文件中。
export function inject<T>(
key: InjectionKey<T> | string | number,
defaultValue?: T,
treatDefaultAsFactory: boolean = false
): T {
if (!currentInstance) {
if (__DEV__) {
warn(`inject() can only be used inside setup().`);
}
return defaultValue as T;
}
const provides = currentInstance.parent?.provides;
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string];
} else if (arguments.length > 1) {
return treatDefaultAsFactory && typeof defaultValue === 'function'
? (defaultValue as Function)()
: defaultValue as T;
} else if (__DEV__) {
warn(`Injection "${String(key)}" not found`);
}
}
这段代码的核心逻辑如下:
- 首先,判断当前是否在
setup
函数中。inject
只能在setup
函数中使用。 - 获取父组件的
provides
对象。 - 在父组件的
provides
对象中查找 key 对应的数据。如果找到了,直接返回。 - 如果没有找到,并且提供了
defaultValue
,则返回defaultValue
。如果treatDefaultAsFactory
为true
,则将defaultValue
视为一个工厂函数,并调用它来生成默认值。 - 如果没有找到,并且没有提供
defaultValue
,则在开发环境下发出警告。
总结:
provide/inject
的实现原理其实很简单:
provide
将数据存储到当前组件实例的provides
对象中。inject
从父组件的provides
对象中查找数据。如果没有找到,则从父组件的原型链上查找。
七、provide/inject
的使用场景
provide/inject
在以下场景中非常有用:
- 主题配置: 可以将主题配置信息通过
provide
提供给所有组件,方便统一管理。 - 国际化: 可以将国际化配置信息通过
provide
提供给所有组件,方便实现国际化。 - 全局状态管理: 虽然 Vuex 和 Pinia 更适合管理复杂的全局状态,但在一些简单的场景下,可以使用
provide/inject
来管理全局状态。 - 组件库: 组件库可以使用
provide/inject
来提供一些公共的配置信息或服务。
八、provide/inject
的注意事项
在使用 provide/inject
时,需要注意以下几点:
provide/inject
只能在setup
函数中使用。inject
只能从父组件或祖先组件中注入数据。不能从兄弟组件或子组件中注入数据。provide/inject
主要用于跨组件层级传递数据,而不是用于组件间通信。如果需要在兄弟组件之间通信,应该使用其他方式,例如mitt
或Vuex
。- 如果需要传递响应式数据,需要使用
ref
或reactive
创建响应式对象,并将其提供给后代组件。 - 使用 Symbol 作为 key 可以避免命名冲突。
九、高级技巧:使用 InjectionKey
类型
Vue 3 提供了一个 InjectionKey
类型,用于定义 provide/inject
的 key。使用 InjectionKey
可以获得更好的类型检查。
import { provide, inject, InjectionKey, ref } from 'vue';
// 定义 InjectionKey
const messageKey: InjectionKey<Ref<string>> = Symbol('message');
export default {
setup() {
const message = ref('爷爷的爱');
provide(messageKey, message);
return {};
},
};
// Child.vue
import { inject, InjectionKey, Ref } from 'vue';
const messageKey: InjectionKey<Ref<string>> = Symbol('message');
export default {
setup() {
const message = inject(messageKey, ref('默认消息'));
return {
message,
};
},
};
在这个例子中,我们使用 InjectionKey
定义了 messageKey
的类型为 Ref<string>
。这样,在 inject
的时候,就可以获得更好的类型检查。
十、总结
provide/inject
是 Vue 3 中一个非常实用的特性,可以方便地实现跨组件层级的数据传递。掌握 provide/inject
的用法和原理,可以帮助我们更好地构建复杂的 Vue 应用。
希望今天的讲解对大家有所帮助!下次有机会再和大家分享其他的 Vue 3 源码知识。 谢谢大家!