Vue中的provide / inject到底是啥

1,477 阅读4分钟

本次想讨论一下Vue的provide / inject,之前其实在业务中一直没有用过这个属性或者很少使用。因为一直找不到这个属性的使用场景,再加上之前忘了哪里看的说这个属性最好少在业务中使用,因为可能会造成数据的来源不清晰,容易混乱数据流。但是我看好多组件开发里面用的这个属性却比较多。再加上前一段时间看到了它们的使用,感觉有点陌生所以就打算在这里跟大家讨论一下。具体使用大家请移步官方文档,其实本文主要讨论两个方面

  1. provide / inject是如何初始化的,整个数据流程是怎么样的?
  2. 为什么provide 和 inject 绑定并不是可响应的?(这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。)

那么就不多啰嗦了,开始吧!

以下源码基于vue 2.6.14版本

1. 初始化数据流程

  • Inject

    1. 首先当我们初始化Vue的时候会初始化initMixin
     function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    initMixin(Vue)
    
    1. initMixin里面
     export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        // 删除一大堆代码
        const vm: Component = this
        vm._self = vm
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    从上面我们可以看到在初始化data前后分别调用了initInjectionsinitProvide两个函数,其实它们就是初始化provide / inject的数据用的。

    我们可以考虑一下为什么Vue初始化各个数据的顺序是这样的,欢迎评论区留言

    initInjections -> initProps -> initMethods -> initData -> initComputed -> initWatch -> initProvide

    1. 下面我们看一下initInjections做了什么
    export function initInjections (vm: Component) {
      const result = resolveInject(vm.$options.inject, vm)
      if (result) {
        toggleObserving(false)
        Object.keys(result).forEach(key => {
          // 删除一些代码
          defineReactive(vm, key, result[key])
        })
        toggleObserving(true)
      }
    }
    

    其实就是把每个组件(实例)的inject对象取出来然后分别defineReactive,下面我们看一下resolveInject做了什么

     // inject: {
    //   foo: {
    //     from: 'bar',
    //     default: 'foo'
    //   }
    // }
    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)
        /**
            在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。
        */
        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...
          // 如果key是__ob__那么就跳过该属性
          if (key === '__ob__') continue
          const provideKey = inject[key].from
          let source = vm
          // 因为provide的属性是每个”子级“都可以取到的,所以需要每个父级去查找最近的provide的值
          while (source) {
            if (source._provided && hasOwn(source._provided, provideKey)) {
              result[key] = source._provided[provideKey]
              break
            }
            source = source.$parent
          }
          // 如果找不到
          if (!source) {
            // 取默认值或者warn
            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
      }
    }
    

    上面的代码其实很好理解,就是根据该组件的inject去相应的“父级”去查到对应provide的值是什么,如果找不到的话就取默认值,还没有的话就warn

     /**
     * Check whether an object has the property.
     */
    const hasOwnProperty = Object.prototype.hasOwnProperty
    export function hasOwn (obj: Object | Array<*>, key: string): boolean {
      return hasOwnProperty.call(obj, key)
    }
    
    export const hasSymbol =
      typeof Symbol !== 'undefined' && isNative(Symbol) &&
      typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)
    
    1. 下面我们看一下defineReactive做了什么
    /**
     * In some cases we may want to disable observation inside a component's
     * update computation.
     */
    export let shouldObserve: boolean = true
    
    export function toggleObserving (value: boolean) {
      shouldObserve = value
    }
    
    /**
     * Define a reactive property on an Object.
     */
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      // configurable:当且仅当该属性为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      // 如果没有提供val就自己去取
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
      // 如果不是浅监听就observe这个val
      let childOb = !shallow && observe(val)
      // 响应式
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          // 放入dep
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                // 如果值是一个数组,则将数组的每一项放入到dep中
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          // observe新设置的值
          childOb = !shallow && observe(newVal)
          // dep通知watcher数据更新了,进而更新视图
          dep.notify()
        }
      })
    }
    

    其实defineReactive是很好理解的,就是把inject的所有值变成响应式的。这里不懂的人可以去看一下Vue的响应式原理(后期如果有时间的话再写一篇文章介绍Vue的响应式)。

     /**
     * Collect dependencies on array elements when the array is touched, since
     * we cannot intercept array element access like property getters.
     */
    function dependArray (value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
          dependArray(e)
        }
      }
    }
    

    将数组的每一项都加入到dep中,下面看一下observe的具体实现

     /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      // 不是对象或者是组件就return
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      // 响应式对象都会加上__ob__属性
      // 如果已经是响应式的了就直接取响应式对象,不是的话在重新Observer
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if (
        // 这里的shouldObserve,初始化inject的时候设置为了false
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      // 如果是响应式对象则返回,否则返回原始对象
      return ob;
    }
    

    其实到这整个初始化inject的过程就结束了。

    • 不知道大家看到这里有没有疑惑,就是上面的inject的key取值,是从from属性取的const provideKey = inject[key].from,但是我们根本就没有设置from属性,那不就是undefined了吗?那还怎么取呢?其实在一开始初始化的时候Vue都帮你做好了

       // 在Vue调用init的时候会进行mergeOptions操作,里面会调用 normalizeInject(child, vm)
      /**
       * Normalize all injections into Object-based format
       */
      function normalizeInject (options: Object, vm: ?Component) {
        const inject = options.inject
        if (!inject) return
        // 置为空对象
        const normalized = options.inject = {}
        // 这种形式的inject: ['item', 'test', 'user'],
        if (Array.isArray(inject)) {
          for (let i = 0; i < inject.length; i++) {
            normalized[inject[i]] = { from: inject[i] }
          }
        } else if (isPlainObject(inject)) {
          // 这种形式的
          // inject: {
          //   foo: {
          //     from: 'bar',
          //     default: 'foo'
          //   }
          // }
          for (const key in inject) {
            const val = inject[key]
            normalized[key] = isPlainObject(val)
              // 有就用原来的没有就设置
              ? extend({ from: key }, val)
              : { from: val }
          }
        }
      }
      
      /**
       * Mix properties into target object.
       */
      export function extend (to: Object, _from: ?Object): Object {
        for (const key in _from) {
          to[key] = _from[key]
        }
        return to
      }
      

      在上面Vue会为每一个inject的key添加from属性

    小结
    • 总体的流程就是根据组件配置的inject,去它的所有“父级”组件去查找,如果找到了就通过defineReactive将该属性增加到vm里面,然后observe这个值。如果没有找到就使用default的值,再没有就warn
  • Provide

    1. 初始化provide的过程其实很简单

      export function initProvide (vm: Component) {
        const provide = vm.$options.provide
        if (provide) {
          // provide 选项应该是一个对象或返回一个对象的函数。
          // inject取值也是从_provided里面
          vm._provided = typeof provide === 'function'
            ? provide.call(vm)
            : provide
        }
      }
      

      流程就是查看当前组件有没有provide属性,如果有的话就执行provide

2. 为什么provide 和 inject 绑定并不是可响应的?

我的理解是每个Inject在初始化的时候都是一个新的响应式对象和之前的是分开的,相当于把inject的对象作为新的数据放到这个组件上面。但是对于对象类型的数据,因为inject的数据在defineReactive的时候并没有深层的observe所有的key,所以对象形式的修改其实还是“响应式的”。

3. Issue

我们来看一下关于上面的两个issue

  • #6574:其实这个说的是Vue一开始在查找的时候没有忽略__ob__这个属性,导致每次都会报Injection"__ob__" not found的warnning

  • #7981:看了一下评论,好像一开始是因为一个人提了一个issue说在组件初始化时会调用getter,他期望不要一开始就立即调用而是希望自己在需要的时候调用getter(他好像有个延迟加载并调用后端接口的场景)。然后尤大说什么时候调用getter是没有保证的并且getter不应该有副作用。然后提这个PR(链接)的人就在下面评论了一个方案。感兴趣的可以自己看一下,我只看明白了个大概。了解的可以在评论区科普一下。

以下源码基于Vue 3.2.29版本

1. 初始化数据流程

  • Inject

    1. 其实整体流程都差不多,我们直接看具体的实现吧
     export function inject(
       key: InjectionKey<any> | string,
       defaultValue?: unknown,
       treatDefaultAsFactory = false
     ) {
       // fallback to `currentRenderingInstance` so that this can be called in
       // a functional component
       const instance = currentInstance || currentRenderingInstance
       if (instance) {
         // #2400
         // to support `app.use` plugins,
         // fallback to appContext's `provides` if the instance is at root
         // 跟vue 2.x同理
         const provides =
           instance.parent == null
             ? instance.vnode.appContext && instance.vnode.appContext.provides
             : instance.parent.provides
     ​
         if (provides && (key as string | symbol) in provides) {
           return provides[key as string]
         } else if (arguments.length > 1) {
           return treatDefaultAsFactory && isFunction(defaultValue)
             ? defaultValue.call(instance.proxy)
             : defaultValue
         }
       }
     }
    

企业也是根据inject的值在组件的“父级”去查找,有的话就赋值,没有的话就取默认值,再没有就warn

  • Provide

     export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
         let provides = currentInstance.provides
         // by default an instance inherits its parent's provides object
         // but when it needs to provide values of its own, it creates its
         // own provides object using parent provides object as prototype.
         // this way in `inject` we can simply look up injections from direct
         // parent and let the prototype chain do the work.
         const parentProvides =
           currentInstance.parent && currentInstance.parent.provides
         if (parentProvides === provides) {
           provides = currentInstance.provides = Object.create(parentProvides)
         }
         // TS doesn't allow symbol as index type
         provides[key as string] = value
     }
    
  • Issue

    #2400:这个主要的原因是使用者的组件inject使用了自己provide的数据,此修复程序确保组件中的注入来自父级。对于根组件,可以注入通过插件提供的属性。