分析provide 和 inject

使用 provide/inject 做全局状态管理

Vue 不会对 provide 中的变量进行响应式处理。所以,要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的。

// 父组件
<template>
  <div id="app">
    Parent组件
    <br />
    <button type="button" @click="changeName">改变name</button>
    <br />
    Parent组件中 name的值: {{ name }}

    <HelloWorld msg="Hello Vue in CodeSandbox!" />
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld";

export default {
  name: "App",
  components: {
    HelloWorld,
  },
  data() {
    return {
      name: "kevin",
      store: {
        login: true
      }
    };
  },
  created() {
    this.userInfo = "created";
  },
  provide() {
    return {
      getReaciveNameFromParent: () => this.name,
      store: () => this.store,
    };
  },
  methods: {
    changeName(val) {
      this.name = "New Kevin";
      this.store.login = false
    },
  },
};
</script>
// HelloWorld
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <ion></ion>
    <h1>{{ msg }}</h1>
    <test></test>
  </div>
</template>

<script>
import ion from "./ion";
import test from "./test";
export default {
  name: "HelloWorld",
  components: {
    ion,
    test,
  },
  props: {
    msg: String,
  },
};
</script>
<template>
  <div class="ion">
    <div class="grandson-container">
      Grandson组件
      <br />
      <br />
      {{ reactiveNameFromParent }}
      <hr />
      test: {{ reactiveStore.login }}
    </div>
  </div>
</template>

<script>
export default {
  name: "ion",
  props: {
    msg: String,
  },
  inject: ["getReaciveNameFromParent", "store"],
  computed: {
    reactiveNameFromParent() {
      return this.getReaciveNameFromParent();
    },
    reactiveStore(){
       return this.store();
    }
  },
  watch: {
    reactiveNameFromParent: function (val) {
      console.log("来自Parent组件的name值发生了变化", val);
    },
  },
};
</script>
<template>
  <div class="test" @click="changeName">
  test: {{ reactiveStore.login }}</div>
</template>

<script>
export default {
  name: "test",
  props: {
    msg: String,
  },
  inject: ["store"],
  computed: {
    reactiveStore(){
       return this.store();
    }
  },
  methods: {
    changeName() {
      console.log("val",  this.reactiveStore.login);
       this.reactiveStore.login = !this.reactiveStore.login
    },
  },
};
</script>

代码预览

Vuex 和 provide/inject 最大的区别在于,Vuex 中的全局状态的每次修改是可以追踪回溯的,而 provide/inject 中变量的修改是无法控制的,换句话说,你不知道是哪个组件修改了这个全局状态。

原理

还记得我们注册全局组件过程中所用到的Vue.extend方法吗。

    Sub.options = mergeOptions(
        Super.options,
        extendOptions
    );

extendOptions 就包含我们inject属性,看看mergeOptions函数:

    function mergeOptions(
        parent,
        child,
        vm
    ) {
        {
            checkComponents(child);
        }

        if (typeof child === 'function') {
            child = child.options;
        }

        normalizeProps(child, vm);
        normalizeInject(child, vm);
        normalizeDirectives(child);

        // Apply extends and mixins on the child options,
        // but only if it is a raw options object that isn't
        // the result of another mergeOptions call.
        // Only merged options has the _base property.
        if (!child._base) {
            if (child.extends) {
                parent = mergeOptions(parent, child.extends, vm);
            }
            if (child.mixins) {
                for (var i = 0, l = child.mixins.length; i < l; i++) {
                    parent = mergeOptions(parent, child.mixins[i], vm);
                }
            }
        }

        var options = {};
        var key;
        for (key in parent) {
            mergeField(key);
        }
        for (key in child) {
            if (!hasOwn(parent, key)) {
                mergeField(key);
            }
        }

        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);
        }
        return options
    }

我们看看这个函数 normalizeInject(child, vm)

  /**
   * Normalize all injections into Object-based format
   */
  function normalizeInject(options, vm) {
      var inject = options.inject;
      if (!inject) {
          return
      }
      var normalized = options.inject = {};
      if (Array.isArray(inject)) {
          for (var i = 0; i < inject.length; i++) {
              normalized[inject[i]] = {
                  from: inject[i]
              };
          }
      } else if (isPlainObject(inject)) {
          for (var key in inject) {
              var val = inject[key];
              normalized[key] = isPlainObject(val) ?
                  extend({
                      from: key
                  }, val) : {
                      from: val
                  };
          }
      } else {
          warn(
              "Invalid value for option \"inject\": expected an Array or an Object, " +
              "but got " + (toRawType(inject)) + ".",
              vm
          );
      }
  }

很简单就是把inject:『parentValue』 挂在了子组件中的options。inject对象下 vm.$options.inject = {"parentValue": {"from": "parentValue"}} 我们在实例化子组件的时候要用到, 看看实例化子组件时对 inject 的处理。 在 _init 时会调用 这个函数

  function initInjections(vm) {
      var result = resolveInject(vm.$options.inject, vm);
      if (result) {
          toggleObserving(false);// 关闭响应式数据定义开关,保证在调用 defineReactive$$1 的时候不对数据进行响应式绑定
          Object.keys(result).forEach(function(key) {
              /* istanbul ignore else */
              {
                  defineReactive$$1(vm, key, result[key], function() {
                      warn(
                          "Avoid mutating an injected value directly since the changes will be " +
                          "overwritten whenever the provided component re-renders. " +
                          "injection being mutated: \"" + key + "\"",
                          vm
                      );
                  });
              }
          });
          toggleObserving(true);
      }
  }

看看这个 resloveInject

 function resolveInject(inject, vm) {
     if (inject) {

         // inject 是 :any 类型因为流没有智能到能够指出缓存
         const result = Object.create(null)
         // 获取 inject 选项的 key 数组
         const keys = hasSymbol ?
             Reflect.ownKeys(inject).filter(key => {
                 /* istanbul ignore next */
                 return Object.getOwnPropertyDescriptor(inject, key).enumerable
             }) :
             Object.keys(inject)

         for (let i = 0; i < keys.length; i++) {
             const key = keys[i]
             const provideKey = inject[key].from
             let source = vm

             // 循环向上, 直到拿到祖先节点中的 provide 值
             while (source) {
                 if (source._provided && provideKey in source._provided) {
                     result[key] = source._provided[provideKey]
                     break
                 }
                 source = source.$parent //关键代码
             }
             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
     }

从这个函数可以看到通过while循环,以及source = source.$parent 找到父组件中的_provided属性,拿到其值,也就拿到父组件提供的provide了。所以说孙组件可以拿到父组件中的数据。

接着是_provide属性

  function initProvide(vm) {
      var provide = vm.$options.provide;
      if (provide) {
          vm._provided = typeof provide === 'function' ?
              provide.call(vm) :
              provide;
      }
  }

在父组件实例化时, 我们也调用了 mergeOptions 对父组件的 provide 属性进行了处理:

if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
} else {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    );
}

这个options 参数里面包含 provide 属性。 看看mergeOptions 函数对 provide 的处理.

strats.provide = mergeDataOrFn;

function mergeDataOrFn(
    parentVal,
    childVal,
    vm
) {
    if (!vm) {
        // in a Vue.extend merge, both should be functions
        if (!childVal) {
            return parentVal
        }
        if (!parentVal) {
            return childVal
        }
        // when parentVal & childVal are both present,
        // we need to return a function that returns the
        // merged result of both functions... no need to
        // check if parentVal is a function here because
        // it has to be a function to pass previous merges.
        return function mergedDataFn() {
            return mergeData(
                typeof childVal === 'function' ? childVal.call(this, this) : childVal,
                typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
            )
        }
    } else {
        return function mergedInstanceDataFn() {
            // instance merge
            var instanceData = typeof childVal === 'function' ?
                childVal.call(vm, vm) :
                childVal;
            var defaultData = typeof parentVal === 'function' ?
                parentVal.call(vm, vm) :
                parentVal;
            if (instanceData) {
                return mergeData(instanceData, defaultData)
            } else {
                return defaultData
            }
        }
    }
}

vm.$options.provide 就是 mergedInstanceDataFn 函数。通过调用这个函数我们 _provided 就成为了 {"parentValue":"here is parent data"}。

最后更新于

这有帮助吗?