Vue 3源码深度解析之:`provide/inject`:如何实现跨组件层级的依赖注入。

各位老铁,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里一个特别有意思的家伙:provide/inject

这玩意儿,说白了,就是 Vue 里面的“隔空传功”,让组件之间可以跨层级传递数据,而不用一层层地 props 传下去。想象一下,你爷爷想给孙子发个红包,不用先给爸爸,再给儿子,直接微信转账,就这么痛快!

一、provide/inject:解决什么问题?

在复杂的 Vue 应用中,组件嵌套层级很深是很常见的。如果父组件需要传递数据给很深层的子组件,传统的做法是通过 props 一层层传递。这种方式有两个问题:

  1. 代码冗余: 中间组件可能并不需要这些数据,但为了传递下去,不得不声明 props,增加了代码的噪声。
  2. 维护困难: 如果数据来源发生变化,需要修改所有中间组件的 props 定义,维护成本很高。

provide/inject 就是来解决这些问题的。它允许祖先组件“提供”(provide)数据,后代组件“注入”(inject)数据,而不需要中间组件参与。

二、provide/inject 的基本用法

先看个简单的例子:

// Grandfather.vue (祖父组件)
<template>
  <div>
    <p>我是爷爷</p>
    <Child />
  </div>
</template>

<script>
import { provide } from 'vue';
import Child from './Child.vue';

export default {
  components: {
    Child,
  },
  setup() {
    provide('message', '爷爷的爱'); // 提供一个名为 'message' 的数据
    provide('age', 70); // 提供一个名为 'age' 的数据

    return {};
  },
};
</script>

// Child.vue (孙子组件)
<template>
  <div>
    <p>我是孙子,收到了:{{ message }},爷爷今年{{ age }}岁。</p>
  </div>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message'); // 注入名为 'message' 的数据
    const age = inject('age'); // 注入名为 'age' 的数据

    return {
      message,
      age,
    };
  },
};
</script>

在这个例子中,Grandfather.vue 组件通过 provide 提供了 messageage 两个数据。Child.vue 组件通过 inject 注入了这两个数据,并直接使用。中间的父组件根本不需要知道这些数据的存在。

三、provide 的多种用法

provide 可以接收两种类型的参数:

  1. 字符串 key: 上面的例子就是用的字符串 key。
  2. Symbol key: 使用 Symbol 可以避免命名冲突。
// 使用 Symbol key
import { provide, ref, computed, reactive, toRefs } from 'vue';

const messageKey = Symbol('message');
const ageKey = Symbol('age');
const userKey = Symbol('user');

export default {
  setup() {
    const message = ref('爷爷的爱');
    const age = ref(70);
    const user = reactive({ name: '老王', gender: '男' });

    provide(messageKey, message);
    provide(ageKey, age);
    provide(userKey, toRefs(user)); // 提供响应式对象

    return {};
  },
};

四、inject 的多种用法

inject 方法可以接收三个参数:

  1. key: 必须,用于指定要注入的数据的 key。
  2. defaultValue: 可选,如果祖先组件没有提供对应 key 的数据,则使用该默认值。
  3. isFactory: 可选,一个布尔值,表明 defaultValue 是否为一个工厂函数。如果 isFactorytrue,则 defaultValue 会被视为一个函数,并被调用来生成默认值。
// inject 示例
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message', '默认消息'); // 提供默认值
    const age = inject('age', () => 18, true); // 提供工厂函数作为默认值

    return {
      message,
      age,
    };
  },
};

五、响应式数据的 provide/inject

provide/inject 不仅可以传递静态数据,还可以传递响应式数据。这意味着,如果祖先组件提供的数据发生变化,后代组件也会自动更新。

// Grandfather.vue
<template>
  <div>
    <p>我是爷爷,今年 {{ age }} 岁</p>
    <button @click="increaseAge">长一岁</button>
    <Child />
  </div>
</template>

<script>
import { provide, ref } from 'vue';
import Child from './Child.vue';

export default {
  components: {
    Child,
  },
  setup() {
    const age = ref(70);
    provide('age', age); // 提供响应式数据

    const increaseAge = () => {
      age.value++;
    };

    return {
      increaseAge,
    };
  },
};
</script>

// Child.vue
<template>
  <div>
    <p>我是孙子,爷爷今年 {{ age }} 岁。</p>
  </div>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    const age = inject('age'); // 注入响应式数据

    return {
      age,
    };
  },
};
</script>

在这个例子中,age 是一个响应式数据。当 Grandfather.vue 组件中的 age 发生变化时,Child.vue 组件也会自动更新。

六、源码分析:provide/inject 的实现原理

现在,咱们来扒一扒 provide/inject 的源码,看看它是怎么实现的。

1. provide 的实现

在 Vue 3 中,provide 的实现主要在 packages/runtime-core/src/apiInject.ts 文件中。

export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`);
    }
    return;
  }

  let provides = currentInstance.provides;
  // by default an instance inherits its parent's provides object
  // unless the instance is a root node which returns `null`
  if (provides === parentProvides) {
    provides = currentInstance.provides = Object.create(parentProvides);
  }
  // TS doesn't allow symbol as index type
  provides[key as string] = value;
}

这段代码的核心逻辑如下:

  • 首先,判断当前是否在 setup 函数中。provide 只能在 setup 函数中使用。
  • 获取当前组件实例的 provides 对象。每个组件实例都有一个 provides 对象,用于存储它提供的数据。
  • 如果 provides 对象和父组件的 provides 对象相同,说明当前组件还没有提供任何数据。此时,创建一个新的 provides 对象,并将其原型指向父组件的 provides 对象。这样做的好处是,如果当前组件没有提供某个 key 的数据,可以从父组件的 provides 对象中查找。这就是原型链查找。
  • 将 key 和 value 存储到 provides 对象中。

2. inject 的实现

inject 的实现也在 packages/runtime-core/src/apiInject.ts 文件中。

export function inject<T>(
  key: InjectionKey<T> | string | number,
  defaultValue?: T,
  treatDefaultAsFactory: boolean = false
): T {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`inject() can only be used inside setup().`);
    }
    return defaultValue as T;
  }

  const provides = currentInstance.parent?.provides;

  if (provides && (key as string | symbol) in provides) {
    // TS doesn't allow symbol as index type
    return provides[key as string];
  } else if (arguments.length > 1) {
    return treatDefaultAsFactory && typeof defaultValue === 'function'
      ? (defaultValue as Function)()
      : defaultValue as T;
  } else if (__DEV__) {
    warn(`Injection "${String(key)}" not found`);
  }
}

这段代码的核心逻辑如下:

  • 首先,判断当前是否在 setup 函数中。inject 只能在 setup 函数中使用。
  • 获取父组件的 provides 对象。
  • 在父组件的 provides 对象中查找 key 对应的数据。如果找到了,直接返回。
  • 如果没有找到,并且提供了 defaultValue,则返回 defaultValue。如果 treatDefaultAsFactorytrue,则将 defaultValue 视为一个工厂函数,并调用它来生成默认值。
  • 如果没有找到,并且没有提供 defaultValue,则在开发环境下发出警告。

总结:

provide/inject 的实现原理其实很简单:

  • provide 将数据存储到当前组件实例的 provides 对象中。
  • inject 从父组件的 provides 对象中查找数据。如果没有找到,则从父组件的原型链上查找。

七、provide/inject 的使用场景

provide/inject 在以下场景中非常有用:

  • 主题配置: 可以将主题配置信息通过 provide 提供给所有组件,方便统一管理。
  • 国际化: 可以将国际化配置信息通过 provide 提供给所有组件,方便实现国际化。
  • 全局状态管理: 虽然 Vuex 和 Pinia 更适合管理复杂的全局状态,但在一些简单的场景下,可以使用 provide/inject 来管理全局状态。
  • 组件库: 组件库可以使用 provide/inject 来提供一些公共的配置信息或服务。

八、provide/inject 的注意事项

在使用 provide/inject 时,需要注意以下几点:

  • provide/inject 只能在 setup 函数中使用。
  • inject 只能从父组件或祖先组件中注入数据。不能从兄弟组件或子组件中注入数据。
  • provide/inject 主要用于跨组件层级传递数据,而不是用于组件间通信。如果需要在兄弟组件之间通信,应该使用其他方式,例如 mittVuex
  • 如果需要传递响应式数据,需要使用 refreactive 创建响应式对象,并将其提供给后代组件。
  • 使用 Symbol 作为 key 可以避免命名冲突。

九、高级技巧:使用 InjectionKey 类型

Vue 3 提供了一个 InjectionKey 类型,用于定义 provide/inject 的 key。使用 InjectionKey 可以获得更好的类型检查。

import { provide, inject, InjectionKey, ref } from 'vue';

// 定义 InjectionKey
const messageKey: InjectionKey<Ref<string>> = Symbol('message');

export default {
  setup() {
    const message = ref('爷爷的爱');
    provide(messageKey, message);

    return {};
  },
};

// Child.vue
import { inject, InjectionKey, Ref } from 'vue';

const messageKey: InjectionKey<Ref<string>> = Symbol('message');

export default {
  setup() {
    const message = inject(messageKey, ref('默认消息'));

    return {
      message,
    };
  },
};

在这个例子中,我们使用 InjectionKey 定义了 messageKey 的类型为 Ref<string>。这样,在 inject 的时候,就可以获得更好的类型检查。

十、总结

provide/inject 是 Vue 3 中一个非常实用的特性,可以方便地实现跨组件层级的数据传递。掌握 provide/inject 的用法和原理,可以帮助我们更好地构建复杂的 Vue 应用。

希望今天的讲解对大家有所帮助!下次有机会再和大家分享其他的 Vue 3 源码知识。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注