解释 Vue 3 源码中 `provide` 和 `inject` 机制的底层实现,特别是它们如何在组件树中进行数据的查找和响应式传递。

各位观众,晚上好!我是你们今晚的Vue 3源码导读小助手,今天咱们就来聊聊Vue 3里边那个有点“神秘”但又特别实用的 provideinject

咱们先来定个基调,provideinject 就像是组件树里的“广播台”和“收音机”。祖先组件通过 provide 广播一些信息(数据,方法啥的),后代组件通过 inject 接收这些信息,实现跨层级组件通信,而且还能保持响应式!是不是有点意思?

一、provide:我是祖宗,我发福利!

首先,咱们来看看 provide 是怎么工作的。简单来说,provide 就是在组件实例上注册一些数据,供后代组件使用。

1.1 provide 的两种用法

Vue 3 提供了两种 provide 的写法:

  • 对象形式: 简单粗暴,直接提供一个对象。

    // 父组件
    import { provide, ref } from 'vue';
    
    export default {
      setup() {
        const message = ref('Hello from parent!');
        provide('message', message); // 提供一个名为 'message' 的响应式数据
        provide('author', '张三');    //提供一个名为‘author’的普通数据
        return {};
      }
    }
  • 函数形式: 灵活多变,可以根据需要动态提供数据。

    // 父组件
    import { provide, ref } from 'vue';
    
    export default {
      setup() {
        const count = ref(0);
        provide('getCount', () => count.value); // 提供一个获取 count 值的函数
    
        //每隔一秒count加1
        setInterval(() => {
          count.value++
        }, 1000);
    
        return {};
      }
    }

1.2 源码剖析:provide 到底干了啥?

provide 的核心逻辑其实隐藏在组件的 setup 函数执行过程中。 当我们使用 provide 时,实际上是在当前组件实例的 provides 属性上存储数据。注意,这里说的组件实例是指 VNode 对应的组件实例,而不是简单的 JavaScript 对象。

让我们简化一下源码(为了方便理解,省去了一些边界条件和性能优化):

// 简化版 provide 实现
function provide(key, value) {
  if (!currentInstance) {
    //在 setup 函数之外调用 provide,会报错。
    console.warn(`provide() can only be used inside setup().`);
    return;
  }

  let provides = currentInstance.provides; // 获取当前组件实例的 provides 属性

  // 如果 provides 属性和父组件的 provides 属性相等,说明是第一次 provide
  // 此时,需要创建一个原型指向父组件 provides 的对象,防止修改子组件的 provide 影响到父组件
  if (provides === currentInstance.parent?.provides) {
    provides = currentInstance.provides = Object.create(provides);
  }

  provides[key] = value; // 将 key-value 存储到 provides 对象中
}

几个关键点:

  • currentInstance:指向当前正在执行 setup 函数的组件实例。Vue 3 通过一个全局变量来维护当前组件实例,有点像一个临时的“上下文”。
  • provides:每个组件实例都有一个 provides 属性,它是一个对象,用于存储 provide 提供的键值对。
  • 原型链大法: 如果当前组件是第一次 provide,那么 Vue 会创建一个新的 provides 对象,并将它的原型指向父组件的 provides 对象。这样做的目的是为了实现“继承”,并且隔离父子组件的 provide。也就是说,子组件修改自己provides里的数据,不会影响到父组件的provides。 如果不这样做,那么子组件直接修改父组件的 provides 对象,会导致父组件的状态也发生改变,这显然是不合理的。

二、inject:我是孙子,我来领福利!

现在,咱们来看看 inject 是怎么接收 provide 提供的福利的。

2.1 inject 的用法

inject 允许后代组件接收祖先组件通过 provide 提供的数据。

// 子组件
import { inject, onMounted } from 'vue';

export default {
  setup() {
    const message = inject('message', 'Default message'); // 接收名为 'message' 的数据,如果没有提供,则使用默认值 'Default message'
    const getCount = inject('getCount'); // 接收名为 'getCount' 的函数

    onMounted(() => {
      if (getCount) {
        console.log('Count:', getCount());
      }
    });

    return {
      message,
    };
  }
}

inject 接收两个参数:

  • key:要接收的数据的键名,和 provide 时的键名对应。
  • defaultValue(可选):如果祖先组件没有提供对应的数据,则使用默认值。

2.2 源码剖析:inject 是怎么找到福利的?

inject 的实现稍微复杂一些,因为它需要在组件树中向上查找 provide 提供的数据。

简化版源码:

// 简化版 inject 实现
function inject(key, defaultValue) {
  if (!currentInstance) {
    console.warn(`inject() can only be used inside setup().`);
    return;
  }

  // 从父组件开始,沿着原型链向上查找 provides
  let provides = currentInstance.parent?.provides;

  while (provides && !(key in provides)) {
    provides = Object.getPrototypeOf(provides);
  }

  if (provides && key in provides) {
    return provides[key]; // 找到了,返回对应的值
  } else if (arguments.length > 1) {
    return defaultValue; // 没找到,返回默认值
  } else {
    console.warn(`Injection "${key}" not found`);
  }
}

关键点:

  • 原型链查找: inject 从当前组件的父组件开始,沿着原型链向上查找 provides 对象,直到找到包含指定 keyprovides 对象为止。
  • 默认值: 如果在整个组件树中都没有找到对应的 key,并且提供了 defaultValue,则返回 defaultValue
  • 警告: 如果没有找到对应的 key,也没有提供 defaultValue,则会发出警告。

三、响应式传递:福利不能过期!

provideinject 最厉害的地方在于,它能够保持数据的响应式。也就是说,如果 provide 提供的数据是响应式的(例如,通过 refreactive 创建),那么 inject 接收到的数据也会是响应式的。

3.1 响应式原理

Vue 3 的响应式系统基于 Proxy 实现。当我们通过 refreactive 创建响应式数据时,Vue 会对数据进行“代理”,拦截数据的读取和修改操作,并在数据发生变化时通知相关的组件进行更新。

provide 提供响应式数据时,实际上是将 Proxy 对象存储到 provides 对象中。inject 接收到的也是这个 Proxy 对象。因此,当响应式数据发生变化时,inject 接收数据的组件也会收到通知,并进行更新。

3.2 示例

// 父组件
import { provide, ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    provide('count', count);

    setInterval(() => {
      count.value++;
    }, 1000);

    return {};
  }
};

// 子组件
import { inject } from 'vue';

export default {
  setup() {
    const count = inject('count');

    return {
      count,
    };
  },
  template: `<div>Count: {{ count }}</div>`
};

在这个例子中,父组件通过 provide 提供了响应式数据 count。子组件通过 inject 接收到了 count。当父组件的 count 值发生变化时,子组件的视图也会自动更新。

四、provide/inject 的使用场景

provide/inject 主要用于以下场景:

  • 跨层级组件通信: 当需要将数据从祖先组件传递给后代组件,但中间组件不需要使用这些数据时,可以使用 provide/inject。避免了逐层传递 props 的麻烦。
  • 主题配置: 可以使用 provide/inject 在根组件中提供主题配置,然后在后代组件中使用这些配置。
  • 插件开发: 插件可以使用 provide/inject 向组件树中注入一些功能或服务。

五、provide/inject 的注意事项

  • 非响应式数据: 如果 provide 提供的是非响应式数据(例如,普通字符串、数字等),那么 inject 接收到的数据也是非响应式的。
  • 依赖注入: provide/inject 本质上是一种依赖注入的方式。过度使用依赖注入可能会导致代码难以维护和调试。
  • Symbol 作为 Key: 可以使用 Symbol 作为 provideinject 的 key,以避免命名冲突。

六、深入源码:createApprootContext 的关联

createApp 是 Vue 3 中创建应用程序的入口函数。它会创建一个应用程序实例,并将根组件挂载到指定的 DOM 元素上。

createApp 的过程中,Vue 会创建一个 rootContext 对象,并将它作为根组件的 provides 属性。这意味着,所有组件都可以通过 inject 访问 rootContext 中存储的数据。

这个机制为插件开发提供了一种便捷的方式。插件可以通过修改 rootContext,向整个应用程序注入一些全局的功能或服务。

七、总结

provideinject 是 Vue 3 中一个强大的跨层级组件通信机制。它允许祖先组件向后代组件提供数据,并且能够保持数据的响应式。

特性 provide inject
作用 祖先组件提供数据给后代组件 后代组件接收祖先组件提供的数据
使用方式 对象形式或函数形式 inject(key, defaultValue)
查找方式 N/A 沿着组件树向上查找 provides 对象,直到找到包含指定 keyprovides 对象为止
响应式 如果提供的数据是响应式的,那么接收到的数据也是响应式的 如果祖先组件提供的数据是响应式的,那么接收到的数据也是响应式的
适用场景 跨层级组件通信、主题配置、插件开发 跨层级组件通信、主题配置、插件开发
注意事项 可以使用 Symbol 作为 key,以避免命名冲突。如果 provide 提供的是非响应式数据,那么 inject 接收到的数据也是非响应式的。过度使用依赖注入可能会导致代码难以维护和调试。 可以使用 Symbol 作为 key,以避免命名冲突。如果祖先组件提供的是非响应式数据,那么接收到的数据也是非响应式的。过度使用依赖注入可能会导致代码难以维护和调试。如果没有找到对应的 key,也没有提供 defaultValue,则会发出警告。
源码实现 存储到组件实例的 provides 属性中 从当前组件的父组件开始,沿着原型链向上查找 provides 对象
与 createApp 的关联 通过 createApp 创建应用程序时,Vue 会创建一个 rootContext 对象,并将它作为根组件的 provides 属性。这意味着,所有组件都可以通过 inject 访问 rootContext 中存储的数据 通过 createApp 创建应用程序时,Vue 会创建一个 rootContext 对象,并将它作为根组件的 provides 属性。这意味着,所有组件都可以通过 inject 访问 rootContext 中存储的数据

希望今天的讲解能够帮助大家更好地理解 Vue 3 中 provideinject 的底层实现。 记住,理解源码才能更好地使用框架,写出更优雅的代码!

感谢大家的收看,咱们下期再见!

发表回复

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