Provide,Inject

61 阅读3分钟
prop 逐级透传问题

通常情况下,当我们需要从父组建向子组件传递参数时会用到prop。但是一个多层级嵌套的组件,会形成一个巨大的组建树,如果某一个深层的子组件需要用到某个较远的祖先组件的一些数据,仅通过props一层一层的向下传递,会非常的麻烦,如果这个组件链路上非常的长,就是影响到更多在这条链路上的组件。为了更新方便的解决组件跨层级的传参,一个父组件相对于它所有的后代组件,会作为 依赖提供者(provide。任何后代的组件树,无论层级有多深,都能 注入(inject 由父组件提供给整条链路的依赖。

2.0 和 3.0 实现原理是有区别的,前者是 while 语句循环,沿着组件树逐级向上查找。后者则是通过原型链向上查找。用法上基本一样

2.0

provide 默认不会使数据成为响应式,inject 依赖的数据默认是响应式的,经过了 defineReactive 处理。

// 它会沿着组件树一层一层由内到外的查找,直到根组件
function resolveInject (inject, vm) {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    var result = Object.create(null);
    var keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject);

    for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      // #6574 in case the inject object is observed...
      if (key === '__ob__') { continue }
      var provideKey = inject[key].from;
      var source = vm;
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey];
          break
        }
        source = source.$parent;
      }
      if (!source) {
        if ('default' in inject[key]) {
          var provideDefault = inject[key].default;
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault;
        } else {
          warn(("Injection \"" + key + "\" not found"), vm);
        }
      }
    }
    return result
  }
}

// inject 初始化,在前,可以被 data 选项用到
function initInjections (vm) {
  var result = resolveInject(vm.$options.inject, vm);
  if (result) {
    toggleObserving(false);
    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);
  }
}

// provde 初始化,在后,可以用到 data 里面的数据
// 每个组件实例的 provide 都是相互独立的。
function initProvide (vm) {
  var provide = vm.$options.provide;
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide;
  }
}
3.0

provide 默认不会使数据成为响应式,inject 依赖的数据是根据注入来定的。 为保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性。ref, reactive, 都可

// 当一个组件被创建时, provides 属性初始化的时候,
// 如果父组件存在,则取父组件的 provide,否则,则继承根组件的 provide。
function createComponentInstance(vnode, parent, suspense){
    ...
    const instance = {
        ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        ...
    }
    ...
    return instance;
}

// 依赖
// 调用 provide 方法时,会先获取当前组件实例的 provide 属性,同时获取父组件的 provide。
// 如果两者相等,说明当前组件实例没有属于自己的 provide,现在的 provide 来源于初始化(如上所示,
// 这时需要通过 Object.create() 给当前组件的 provide 重新设置为继承父组件的对象。
// 然后给通过 key,value 的方式复值。
function provide(key, value) {
    if (!currentInstance) {
        if ((process.env.NODE_ENV !== 'production')) {
            warn(`provide() can only be used inside setup().`);
        }
    }
    else {
        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] = value;
    }
}

// 注入
// 当注入的时候,会去先获取当前组件的父组件,如果父组件存,在则说明父组件有依赖 provide,直接返回或者沿着原型链查找,有就返回。如果父组件不存,就直接去根组件中查找,有就返回。如果都没有,就返回默认值,没有默认值就说明这个注入是无效的,开发环境会提示报错,值为 null。
function inject(key, defaultValue, 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
        const provides = instance.parent == null
            ? instance.vnode.appContext && instance.vnode.appContext.provides
            : instance.parent.provides;
        if (provides && key in provides) {
            // TS doesn't allow symbol as index type
            return provides[key];
        }
        else if (arguments.length > 1) {
            return treatDefaultAsFactory && isFunction(defaultValue)
                ? defaultValue.call(instance.proxy)
                : defaultValue;
        }
        else if ((process.env.NODE_ENV !== 'production')) {
            warn(`injection "${String(key)}" not found.`);
        }
    }
    else if ((process.env.NODE_ENV !== 'production')) {
        warn(`inject() can only be used inside setup() or functional components.`);
    }
}