provide
和inject
选项需要一起使用,它允许祖先组件向其所有子孙组件注入依赖,并在其上下游关系成立的时间里始终生效,不论组件层级有多深。
1. 我们简单回顾一下provide/inject的使用方式
如下:
var Provider = {
provide: {
foo: "bar"
}
}
var Child = {
inject: ["foo"],
created() {
console.log(this.foo); // "bar"
}
}
如果使用了ES5 Symbol
作为key,则使用方式如下:
const s = Symbol();
var Provider = {
provide() {
return {
[s]: "bar"
}
}
}
var Child = {
inject: {
s },
created() {
console.log(this.foo); // "bar"
}
}
可以在data/props
中访问注入的值:
var Provider = {
provide: {
foo: "bar",
foo2: "bar2"
}
}
var Child = {
inject: ["foo"],
props: {
bar: {
default() {
return this.foo;
}
}
},
data() {
return {
bar2: this.foo2
}
}
}
可以设置inject
的默认值:
var Provider = {
provide: {
foo: "bar"
}
}
var Child = {
inject: {
foo: {
default: "foo"}
}
}
如果它需要从一个不同名字的属性注入,则使用from
来表示其源属性:
var Provider = {
provide: {
foo: "bar"
}
}
var Child = {
inject: {
foo: {
from: "var",
default: "foo"
}
}
}
2.inject的内部原理
inject
在data/props
之前初始化,而provide
在data/props
之后初始化。这样做的目的是让用户可以在data/props
中使用inject
所注入的内容。也就是说,为了data/props
依赖inject
,需要将初始化inject
放在初始化data/props
的前面。
首先,我们定义初始化inject
的函数:
export function initInjections (vm: Component) {
// 通过用户配置的inject,自底向上搜索可用的注入内容,并将搜索结果返回
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false) // 通知defineReactive不要将内容转换成响应式
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
}
resolveInject
函数的作用是通过用户配置的inject
,自底向上搜索可用的注入内容,并将搜索结果返回。而toggleObserving(false)
的作用是通知defineReactive不要将内容转换成响应式。
那么resolveInject
是如何实现的呢?实现这个函数的主要思想是:读出用户在当前组件中设置的inject
的key
,然后循环key
,将每一个key
从当前组件起,不断向父组件查找是否有值,找到了就停止循环,最终将所有key
对应的值一起返回即可。
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
/**
* hasSymbol:是否支持Symbol
*/
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
return result
}
}
第一步定义一个对象result
来存储我们的结果,然后将inject
的key
读出来。如果是支持Symbol
的情况下就使用Reflect.ownKeys(inject)
读取,如果不支持的话就使用Object.keys(inject)
读取。
但是我们知道inject
是支持数组的形式,如果使用了数组形式,如:
var Provider = {
provide: {
foo: "bar"
}
}
var Child = {
inject: ["foo"],
created() {
console.log(this.foo); // "bar"
}
}
这样是不是就不能正确读取inject
中的所有key
了?
其实在Vue.js
实例化的第一步是规格化用户传入的数据,如果inject
传递的内容是数组,那么数据会被规格化成对象并存放在from
属性中。
如果用户设置的inject
是这样的:
{
inject: ["foo"]
}
那么规格化之后将会是下面这样:
{
inject: {
foo: {
from: "foo"
}
}
}
接下来需要循环key
,将每一个key
从当前组件起,不断向父组件查找是否有值,找到了就停止循环,最终将所有key
对应的值一起返回即可。
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
/**
* hasSymbol:是否支持Symbol
*/
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
/*
* 通过from属性得到provide源属性
* 当Vue.js被实例化时,会在上下文(this)中添加$options属性,这会把inject中提供的数据规格化,包括inject
* 用户设置: { inject: [foo] }
* 规格化:{ inject: { foo: { from: "foo" }}}
*/
const provideKey = inject[key].from
let source = vm // 一开始为当前实例
// 自底向上寻找provide源属性
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent // 向上寻找
}
}
return result
}
}
在上述代码中,最外层使用for循环keys,在循环体内依次得到key,然后通过inject[key].from
得到provide源属性provideKey
。然后通过源属性使用while循环来搜索内容。最开始source
是当前组件实例,如果原始属性在source
的_provided
中能找到对应的值,那么将其设置到result
,并使用break退出循环。否则,将source设置为父组件实例进行下一轮循环。
如果provide
没有提供注入,那么将使用inject中的默认值配置,代码如下:
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
/**
* hasSymbol:是否支持Symbol
*/
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
/*
* 通过from属性得到provide源属性
* 当Vue.js被实例化时,会在上下文(this)中添加$options属性,这会把inject中提供的数据规格化,包括inject
* 用户设置: { inject: [foo] }
* 规格化:{ inject: { foo: { from: "foo" }}}
*/
const provideKey = inject[key].from
let source = vm // 一开始为当前实例
// 自底向上寻找provide源属性
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent // 向上寻找
}
// 没有source,设置默认值
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
// 支持函数和普通字符串
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${
key}" not found`, vm)
}
}
}
return result
}
}
如果循环结束后source
为空,那么可以确定provide
没有提供相应值注入,这时候就需要读取inject
中配置的默认值。如果'default' in inject[key]
,证明配置了默认值,如果没有,将会在生产环境下打印警告。通过inject[key].default
读取到provideDefault
,但是默认值是支持函数和普通字符串的,这个时候需要判断provideDefault
是不是函数,如果是就执行它并将结果存入result中;如果不是就直接将provideDefault
存入result中。
3. 完整代码如下:
/**
* 通过用户配置的inject,自底向上搜索可用的注入内容,并将搜索结果返回
* 当使用provide注入内容时,其实是将内容注入到当前组件实例的_provide中,所以inject可以从父组件实例的_provide中获取注入的内容
*/
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
/**
* hasSymbol:是否支持Symbol
*/
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
/*
* 通过from属性得到provide源属性
* 当Vue.js被实例化时,会在上下文(this)中添加$options属性,这会把inject中提供的数据规格化,包括inject
* 用户设置: { inject: [foo] }
* 规格化:{ inject: { foo: { from: "foo" }}}
*/
const provideKey = inject[key].from
let source = vm // 一开始为当前实例
// 自底向上寻找provide源属性
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent // 向上寻找
}
// 没有source,设置默认值
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
// 支持函数和普通字符串
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${
key}" not found`, vm)
}
}
}
return result
}
}