Vue3全局组件通信之provide / inject

文章详细介绍了Vue中爷孙组件间的通信机制,重点讲解了provide/inject的使用方法,包括在Vue2和3中的变化。提供和注入数据时,需注意响应式和非响应式数据的区别,以及如何处理基本类型和引用类型的数据。同时强调了provide/inject的非响应式特性及其应对策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、前言

顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件”):

C组件引入到B组件里,B组件引入到A组件里渲染,此时A是C的爷爷级别(可能还有更多层级关系),如果你用 props ,只能一级一级传递下去,那就太繁琐了,因此我们需要更直接的通信方式。

他们之间的关系如下,Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (可能有多个):

Grandfather.vue
└─Son.vue
  └─Grandson.vue

😜😜😜
因为上下级的关系的一致性,爷孙组件通信的方案也适用于 父子组件通信 ,只需要把爷孙关系换成父子关系即可。

2、provide / inject

这个特性有两个部分:Grandfather.vue 有一个 provide 选项来提供数据,Grandson.vue 有一个 inject 选项来开始使用这些数据。

  • Grandfather.vue 通过 provide 向 Grandson.vue 传值(可包含定义好的函数)
  • Grandson.vue 通过 inject 向 Grandfather.vue 触发爷爷组件的事件执行

😜😜😜
无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者
这一部分的内容变化都特别大,但使用起来其实也很简单,不用慌,也有相同的地方:

  • 父组件不需要知道哪些子组件使用它 provide 的 property
  • 子组件不需要知道 inject property 来自哪里

另外,要切记一点就是:provide 和 inject 绑定并不是可响应的。这是刻意为之的,但如果传入了一个可监听的对象,那么其对象的 property 还是可响应的。

3、发起 provide

我们先来回顾一下 2.x 的用法:

export default {
  // 定义好数据
  data () {
    return {
      tags: [ '中餐', '粤菜', '烧腊' ]
    }
  },
  // provide出去
  provide () {
    return {
      tags: this.tags
    }
  }
}

旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数。
3.x 的新版 provide, 和 2.x 的用法区别比较大。

😜😜😜
在 3.x , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法。
每次要 provide 一个数据的时候,就要单独调用一次。
每次调用的时候,都需要传入 2 个参数:

参数类型说明
keystring数据的名称
valueany数据的值

来看一下如何创建一个 provide:

// 记得导入provide
import { defineComponent, provide } from 'vue'

export default defineComponent({
  // ...
  setup () {
    // 定义好数据
    const msg: string = 'Hello World!';

    // provide出去
    provide('msg', msg);
  }
})

操作非常简单对吧,但需要注意的是,provide 不是响应式的,如果你要使其具备响应性,你需要传入响应式数据

4、接收 inject

也是先来回顾一下 2.x 的用法:

export default {
  inject: ['tags'],
  mounted () {
    console.log(this.tags);
  }
}

旧版的 inject 用法和 props 类似,3.x 的新版 inject, 和 2.x 的用法区别也是比较大。

😜😜😜
在 3.x, inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法。
每次要 inject 一个数据的时候,就要单独调用一次。
每次调用的时候,只需要传入 1 个参数:

参数类型说明
keystring与provide对应的数据名称
// 记得导入inject
import { defineComponent, inject } from 'vue'

export default defineComponent({
  // ...
  setup () {
    const msg: string = inject('msg') || '';
  }
})

也是很简单(写 TS 的话,由于 inject 到的值可能是 undefined,所以要么加个 undefined 类型,要么给变量设置一个空的默认值)。

5、响应性数据的传递与接收

之所以要单独拿出来说, 是因为变化真的很大

在前面我们已经知道,provide 和 inject 本身不可响应,但是并非完全不能够拿到响应的结果,只需要我们传入的数据具备响应性,它依然能够提供响应支持。

我们以 ref 和 reactive 为例,来看看应该怎么发起 provide 和接收 inject。

先在 Grandfather.vue 里 provide 数据:

export default defineComponent({
  // ...
  setup () {
    // provide一个ref
    const msg = ref<string>('Hello World!');
    provide('msg', msg);

    // provide一个reactive
    const userInfo: Member = reactive({
      id: 1,
      name: 'Petter'
    });
    provide('userInfo', userInfo);

    // 2s 后更新数据
    setTimeout(() => {
      // 修改消息内容
      msg.value = 'Hi World!';

      // 修改用户名
      userInfo.name = 'Tom';
    }, 2000);
  }
})

在 Grandsun.vue 里 inject 拿到数据:

export default defineComponent({
  setup () {
    // 获取数据
    const msg = inject('msg');
    const userInfo = inject('userInfo');

    // 打印刚刚拿到的数据
    console.log(msg);
    console.log(userInfo);

    // 因为 2s 后数据会变,我们 3s 后再看下,可以争取拿到新的数据
    setTimeout(() => {
      console.log(msg);
      console.log(userInfo);
    }, 3000);

    // 响应式数据还可以直接给 template 使用,会实时更新
    return {
      msg,
      userInfo
    }
  }
})

非常简单,非常方便!!!

😜😜😜
响应式的数据 provide 出去,在子孙组件拿到的也是响应式的,并且可以如同自身定义的响应式变量一样,直接 return 给 template 使用,一旦数据有变化,视图也会立即更新。

但上面这句话有效的前提是,不破坏数据的响应性,比如 ref 变量,你需要完整的传入,而不能只传入它的 value,对于 reactive 也是同理,不能直接解构去破坏原本的响应性

切记!切记!!!
在这里插入图片描述

6、引用类型的传递与接收 (针对非响应性数据的处理)

provide 和 inject 并不是可响应的,这是官方的故意设计,但是由于引用类型的特殊性,在子孙组件拿到了数据之后,他们的属性还是可以正常的响应变化。

先在 Grandfather.vue 里 provide 数据:

export default defineComponent({
  // ...
  setup () {
    // provide 一个数组
    const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
    provide('tags', tags);

    // provide 一个对象
    const userInfo: Member = {
      id: 1,
      name: 'Petter'
    };
    provide('userInfo', userInfo);

    // 2s 后更新数据
    setTimeout(() => {
      // 增加tags的长度
      tags.push('叉烧');

      // 修改userInfo的属性值
      userInfo.name = 'Tom';
    }, 2000);
  }
})

在 Grandsun.vue 里 inject 拿到数据:

export default defineComponent({
  setup () {
    // 获取数据
    const tags: string[] = inject('tags') || [];
    const userInfo: Member = inject('userInfo') || {
      id: 0,
      name: ''
    };

    // 打印刚刚拿到的数据
    console.log(tags);
    console.log(tags.length);
    console.log(userInfo);

    // 因为 2s 后数据会变,我们 3s 后再看下,能够看到已经是更新后的数据了
    setTimeout(() => {
      console.log(tags);
      console.log(tags.length);
      console.log(userInfo);
    }, 3000);
  }
})

😜😜😜
引用类型的数据,拿到后可以直接用,属性的值更新后,子孙组件也会被更新。
但是!!!由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 API 。

7、基本类型的传递与接收 (针对非响应性数据的处理)

基本数据类型被直接 provide 出去后,再怎么修改,都无法更新下去,子孙组件拿到的永远是第一次的那个值。

先在 Grandfather.vue 里 provide 数据:

export default defineComponent({
  // ...
  setup () {
    // provide 一个数组的长度
    const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
    provide('tagsCount', tags.length);

    // provide 一个字符串
    let name: string = 'Petter';
    provide('name', name);

    // 2s 后更新数据
    setTimeout(() => {
      // tagsCount 在 Grandson 那边依然是 3
      tags.push('叉烧');

      // name 在 Grandson 那边依然是 Petter
      name = 'Tom';
    }, 2000);
  }
})

在 Grandsun.vue 里 inject 拿到数据:

export default defineComponent({
  setup () {
    // 获取数据
    const name: string = inject('name') || '';
    const tagsCount: number = inject('tagsCount') || 0;

    // 打印刚刚拿到的数据
    console.log(name);
    console.log(tagsCount);

    // 因为 2s 后数据会变,我们 3s 后再看下
    setTimeout(() => {
      // 依然是 Petter
      console.log(name);

      // 依然是 3
      console.log(tagsCount);
    }, 3000);
  }
})

很失望,并没有变化。

😜😜😜
那么是否一定要定义成响应式数据或者引用类型数据呢?

当然不是,我们在 provide 的时候,也可以稍作修改,让它能够同步更新下去。

先在 Grandfather.vue 里 provide 数据:

export default defineComponent({
  // ...
  setup () {
    // provide 一个数组的长度
    const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
    provide('tagsCount', (): number => {
      return tags.length;
    });

    // provide 字符串
    let name: string = 'Petter';
    provide('name', (): string => {
      return name;
    });

    // 2s 后更新数据
    setTimeout(() => {
      // tagsCount 现在可以正常拿到 4 了
      tags.push('叉烧');

      // name 现在可以正常拿到 Tom 了
      name = 'Tom';
    }, 2000);
  }
})

再来 Grandsun.vue 里修改一下 inject 的方式,看看这次拿到的数据:

export default defineComponent({
  setup () {
    // 获取数据
    const tagsCount: any = inject('tagsCount');
    const name: any = inject('name');

    // 打印刚刚拿到的数据
    console.log(tagsCount());
    console.log(name());

    // 因为 2s 后数据会变,我们 3s 后再看下
    setTimeout(() => {
      // 现在可以正确得到 4
      console.log(tagsCount());

      // 现在可以正确得到 Tom
      console.log(name());
    }, 3000);
  }
})

这次可以正确拿到数据了,看出这2次的写法有什么区别了吗?

😜😜😜
基本数据类型,需要 provide 一个函数,将其 return 出去给子孙组件用,这样子孙组件每次拿到的数据才会是新的。
但由于不具备响应性,所以子孙组件每次都需要重新通过执行 inject 得到的函数才能拿到最新的数据。

按我个人习惯来说,使用起来挺别扭的,能不用就不用……

由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 API 。

点赞加关注,永远不迷路

点赞加关注,永远不迷路

点赞加关注,永远不迷路

<think>我们已经在之前的讨论中详细说明了Vue3中如何使用provideinject实现跨组件状态共享。现在,根据用户的问题,我们将重点放在Vue3provide/inject的具体实现方法上。根据引用[1]、[2]、[3]、[4]、[5]的内容,我们可以总结如下:1.**provideinject的作用**:它们提供了一种在祖先组件和后代组件之间跨层级共享数据的方式,避免了通过props逐层传递的繁琐。2.**基本用法**:-在祖先组件中使用`provide`函数提供数据或方法。-在后代组件中使用`inject`函数注入祖先组件提供的数据或方法。3.**响应式数据**:在Vue3中,我们可以结合`ref`或`reactive`创建响应式数据,并通过`provide`提供,这样后代组件注入的数据也是响应式的。4.**使用场景**:适合在深层嵌套的组件之间共享状态,比如主题、用户信息、全局配置等。下面我们详细说明实现步骤:###一、基本使用####1.祖先组件提供数据```vue<!--Ancestor.vue--><scriptsetup>import{provide,ref}from&#39;vue&#39;//提供静态数据provide(&#39;siteName&#39;,&#39;Vue3学习网站&#39;)//提供响应式数据constcount=ref(0)provide(&#39;count&#39;,count)//提供方法functionincrement(){count.value++}provide(&#39;increment&#39;,increment)</script>```####2.后代组件注入数据```vue<!--Descendant.vue--><scriptsetup>import{inject}from&#39;vue&#39;//注入数据(第二个参数为默认值,可选)constsiteName=inject(&#39;siteName&#39;,&#39;默认网站名称&#39;)constcount=inject(&#39;count&#39;)constincrement=inject(&#39;increment&#39;)</script><template><div><h1>{{siteName}}</h1><p>计数:{{count}}</p><button@click="increment">增加</button></div></template>```###二、共享响应式状态我们可以将多个状态和方法组织在一个对象中,通过`reactive`或`ref`创建响应式对象,然后提供出去。####1.创建共享状态```javascript//useSharedState.jsimport{reactive,provide,inject}from&#39;vue&#39;//定义状态对象conststate=reactive({count:0,increment(){this.count++}})//提供状态的Symbolkey,避免命名冲突exportconstSharedStateKey=Symbol(&#39;sharedState&#39;)//提供状态的函数exportfunctionprovideSharedState(){provide(SharedStateKey,state)}//注入状态的函数exportfunctionuseSharedState(){constsharedState=inject(SharedStateKey)if(!sharedState){thrownewError(&#39;useSharedStatemustbeusedwithinaprovider&#39;)}returnsharedState}```####2.在祖先组件中提供```vue<!--App.vue--><scriptsetup>import{provideSharedState}from&#39;./useSharedState&#39;provideSharedState()</script>```####3.在后代组件中使用```vue<!--ChildComponent.vue--><scriptsetup>import{useSharedState}from&#39;./useSharedState&#39;const{count,increment}=useSharedState()</script>```###三、处理异步数据根据引用[3],我们可以使用provide/inject来共享异步获取的数据。####1.祖先组件提供异步方法```vue<!--DataProvider.vue--><scriptsetup>import{provide}from&#39;vue&#39;importaxiosfrom&#39;axios&#39;//提供异步获取数据的方法provide(&#39;fetchData&#39;,async()=>{try{constresponse=awaitaxios.get(&#39;/api/data&#39;)returnresponse.data}catch(error){console.error(error)returnnull}})</script>```####2.后代组件注入并使用```vue<!--Consumer.vue--><scriptsetup>import{inject,ref,onMounted}from&#39;vue&#39;constfetchData=inject(&#39;fetchData&#39;)constdata=ref(null)onMounted(async()=>{data.value=awaitfetchData()})</script>```###四、注意事项1.**避免滥用**:在组件树中过度使用provide/inject会导致组件间耦合度增加,建议只在必要的情况下使用(如多层嵌套组件共享状态)[^3]。2.**命名冲突**:建议使用Symbol作为键名,避免多个provide提供相同字符串键名导致冲突[^5]。3.**响应式数据**:如果提供的是响应式数据(如ref或reactive创建的数据),则在后代组件中修改也会影响到祖先组件中的数据。4.**默认值**:在inject时可以提供默认值,以防止未提供的情况。5.**调试**:可以使用VueDevtools查看provide/inject的数据流[^3]。###五、与Vuex/Pinia的对比-**简单场景**:provide/inject适用于相对简单的跨组件状态共享,特别是组件树中特定分支的共享。-**复杂场景**:对于全局状态管理(如用户登录状态、主题等),使用Pinia或Vuex更为合适,因为它们提供了更强大的状态管理、调试工具和插件支持[^1][^4]。通过以上方法,我们可以在Vue3中灵活使用provide/inject实现跨组件的状态共享。这种模式特别适合在组件层级较深且需要共享状态的场景,避免了逐层传递props的麻烦。§§相关问题§§1.Vue3中,如何避免provide/inject导致的组件间过度耦合?2.使用Symbol作为provide的键名有什么好处?3.Vue3provide/injectVue2相比有哪些改进?4.如何在Vue3中实现provide/inject的响应式数据更新?5.如何通过provide/inject实现全局主题切换功能?</think>## Vue3 中使用 provide/inject 实现跨组件状态共享 在 Vue3 中,`provide` 和 `inject` 是实现跨层级组件状态共享的优雅方案,特别适合解决深层嵌套组件间的通信问题。以下是详细实现方法: ### 一、核心实现原理 1. **单向数据流**:祖先组件`提供`数据,后代组件`注入`使用 2. **响应式支持**:结合`ref`/`reactive`实现状态自动更新 3. **类型安全**:使用 TypeScript 时可定义注入类型 ### 二、基础实现步骤 #### 1. 祖先组件提供数据 ```vue <!-- ProviderComponent.vue --> <script setup> import { provide, ref, reactive } from &#39;vue&#39; // 提供基本数据类型(响应式) const counter = ref(0) provide(&#39;counter&#39;, counter) // 提供对象类型(响应式) const userData = reactive({ name: &#39;张三&#39;, role: &#39;管理员&#39; }) provide(&#39;userData&#39;, userData) // 提供方法 function increment() { counter.value++ } provide(&#39;increment&#39;, increment) </script> <template> <ChildComponent /> </template> ``` #### 2. 后代组件注入数据 ```vue <!-- ChildComponent.vue --> <script setup> import { inject } from &#39;vue&#39; // 注入数据(带默认值) const counter = inject(&#39;counter&#39;, ref(0)) // 默认值确保类型安全 // 注入对象 const userData = inject(&#39;userData&#39;) // 注入方法 const increment = inject(&#39;increment&#39;) // 使用注入数据 function updateUser() { userData.name = &#39;李四&#39; // 直接修改响应式数据 } </script> <template> <div> <p>计数: {{ counter }}</p> <p>用户: {{ userData.name }} ({{ userData.role }})</p> <button @click="increment">增加计数</button> <button @click="updateUser">更新用户</button> </div> </template> ``` ### 三、高级用法 #### 1. 使用 Symbol 避免命名冲突 ```typescript // symbols.ts export const COUNTER_KEY = Symbol(&#39;counter&#39;) export const USER_DATA_KEY = Symbol(&#39;userData&#39;) // 提供端 import { COUNTER_KEY } from &#39;./symbols&#39; provide(COUNTER_KEY, counter) // 注入端 const counter = inject(COUNTER_KEY) ``` #### 2. 响应式状态管理 ```vue <script setup> import { provide, reactive, readonly } from &#39;vue&#39; const state = reactive({ theme: &#39;dark&#39;, settings: { fontSize: 16 } }) // 提供只读状态和更新方法 provide(&#39;appState&#39;, readonly(state)) function updateTheme(newTheme) { state.theme = newTheme } provide(&#39;updateTheme&#39;, updateTheme) </script> ``` #### 3. 异步数据共享 ```vue <!-- DataProvider.vue --> <script setup> import { provide, ref } from &#39;vue&#39; import axios from &#39;axios&#39; const data = ref(null) const loading = ref(true) // 提供异步获取的数据 provide(&#39;asyncData&#39;, { data, loading }) axios.get(&#39;/api/data&#39;) .then(res => { data.value = res.data loading.value = false }) </script> <!-- Consumer.vue --> <script setup> const { data, loading } = inject(&#39;asyncData&#39;) </script> ``` ### 四、最佳实践 1. **避免滥用**:只在真正需要跨层级时使用,避免组件过度耦合[^3] 2. **命名规范**:使用统一前缀或命名空间(如`appTheme`) 3. **默认值处理**:注入时提供默认值保证组件独立性 4. **只读限制**:对不希望子组件修改的数据使用`readonly` 5. **类型安全**(TypeScript): ```typescript interface UserData { name: string role: string } const userData = inject<UserData>(&#39;userData&#39;) ``` ### 五、适用场景 vs 不适用场景 | **适用场景** | **不适用场景** | |------------------------|------------------------| | 主题切换 | 父子组件通信 | | 用户信息共享 | 简单状态管理 | | 多层级表单组件 | 全局状态(用 Pinia) | | 复杂组件库中的配置传递 | 非直接祖先/后代关系 | ### 六、调试技巧 1. 使用 Vue Devtools 查看 provide/inject 关系 2. 注入时添加校验逻辑: ```javascript const counter = inject(&#39;counter&#39;, () => { console.warn(&#39;counter 未提供&#39;) return ref(0) }) ``` > 通过合理使用 provide/inject,可实现组件树的灵活状态共享,同时保持组件解耦[^4]。对于复杂全局状态,建议结合 Pinia 使用[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老电影故事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值