深入解读provide/inject原理

provideinject选项需要一起使用,它允许祖先组件向其所有子孙组件注入依赖,并在其上下游关系成立的时间里始终生效,不论组件层级有多深。

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的内部原理

injectdata/props之前初始化,而providedata/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是如何实现的呢?实现这个函数的主要思想是:读出用户在当前组件中设置的injectkey,然后循环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来存储我们的结果,然后将injectkey读出来。如果是支持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
  }
}

猜你喜欢

转载自blog.csdn.net/weixin_50096821/article/details/123535108
今日推荐