Vue的provide和inject源码深度解读

264 阅读3分钟

provide和inject是什么?

inject()

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

provide()

提供一个值,可以被后代组件注入。

总得来说provideinject 主要解决的是跨级组件间的通信问题

使用示例:

import { provide, inject, h } from 'vue';

const ThemeSymbol = Symbol();

const Provider = {
  setup() {
    provide(ThemeSymbol, 'dark');
  },
  render() {
    return this.$slots.default();
  }
}

const Consumer = {
  setup() {
    const theme = inject(ThemeSymbol);
    return { theme };
  },
  render() {
    return h('div', `Theme: ${this.theme}`);
  }
}

const App = {
  components: {
    Provider,
    Consumer
  },
  template: `
    <Provider>
      <Consumer></Consumer>
    </Provider>
  `
}

createApp(App).mount('#app');

源码实现

github1s链接:github1s.com/vuejs/core/…

先看provide源码:

export function provide<T, K = InjectionKey<T> | string | number>(
  key: K,
  value: K extends InjectionKey<infer V> ? V : T,
) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    provides[key as string] = value
  }
}

从当前组件实例找到provides。找当前组件实例父的provides
如果他们是相等的。那么创建一个原型指向父provides对象作为组件实例的provides
(创建一个新对象是为了维护组件自己的provides
(利用原型链的原理,查找属性的时候如果provides对象没有属性。会沿着原型链找。那么就会找到父组件的provides。不断向上查找。)

注意:
每个组件实例都会有一个 provides 属性,这个属性在组件实例创建时就会被初始化

在初始化过程中,如果一个组件没有声明 provide,那么它的 provides 对象会被设置为其父组件的 provides 对象
这个设计使得 inject 可以跨越任何层级的组件来获取值,而无需担心中间组件是否有 provide

下面这行源码才能解释,为什么可以跨组件传递和获取值? 因为组件初始化就根据组件链构建了provides链

创建组件(createComponentInstance)的时候provides属性已经默认赋值了父组件的providesimage.png

再看inject源码:

export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false,
) {
  const instance = currentInstance || currentRenderingInstance
  if (instance || currentApp) {
    const provides = instance
      ? instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides
      : currentApp!._context.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 && instance.proxy)
        : defaultValue
    } 
  } 
}

获取当前组件实例的父组件的provides。并且provides[key]。这时候会沿着provide时候构建的原型链查找并返回。

总结:

明白了组件的provides本质上是利用js的原型链实现这个原理后。
下次面试官问:
如果A -> B -> C 3个组件组件是一个嵌套结构。
那么A和B都provide了一个 foo。
C组件inject('fod')的时候。获取的是A还是B的foo?
就知道 inject本质上是在根据(组件初始化时候)构建的provides链上查找。找到最近一个满足条件(存在查找的key)的上级的provide的属性。