Code前端首页关于Code前端联系我们

Vue3组件传参用props太麻烦?provide还有这些你不知道的用法和坑?

terry 20小时前 阅读数 237 #Vue

Vue3开发到现在,很多开发者还是习惯用props一层一层传数据,可一旦项目里组件嵌套超过3层,中间那些“只负责转手不干活”的组件就会把代码堆得乱七八糟——比如祖宗App.vue存个用户登录状态或者电商后台的主题色,要传到商品页里的价格标签(曾曾孙级别),侧边栏、商品列表容器、商品分类子容器、商品卡片都得加个userStatus或themeColor的props,哪怕它们根本不用这个值,这种时候,provide和inject组成的“跨级直达通道”就特别好用,但很多人只是简单会用个provide传静态值、inject接值,其实它还有响应式、注入别名、依赖注入优化、高阶组件配合这些用法,还有踩坑的风险,今天就好好唠唠。

Vue3 provide/inject的基础逻辑,和props到底有啥本质区别?

很多入门教程会直接说“provide是提供数据,inject是接收数据”,但没讲清楚底层逻辑,导致大家乱用,其实你可以把provide想象成公司的“共享文件库”——不是随便谁都能看到的,只有公司内部的“员工”(也就是当前组件的所有后代组件,包括子、孙、曾孙……不管嵌套多少层)才能申请查看(inject),而props呢,是“一对一指定送件”——必须从某个“寄件人”组件明确指定送到“收件人”子组件,中间不能跳,要是收件人隔得远,中间还得请“邻居组件”(中间层)帮忙转。

不过这里要注意一个官方文档里提过的核心约束:provide提供的数据,默认不是响应式的!哪怕你传的是ref或reactive,如果不小心修改了引用指向(比如把整个reactive对象重新赋值),后代inject接收到的也不会更新,这一点后面讲坑点的时候会细说。

Vue3 provide的第一个进阶用法:响应式传参,到底怎么写才对?

很多人写响应式provide容易踩雷,要么直接传ref.value(传了普通值,肯定不响应),要么随便操作reactive对象但没传对,要么没加readonly保护导致数据被后代乱改,下面分两种情况讲正确的响应式写法,以及为什么要用readonly。

情况1:提供单个ref响应式数据

比如在App.vue里传主题色,这里的主题色可能是用户点击切换的,需要全局更新,直接传整个ref就可以,不能传.value:

// App.vue
import { ref, provide } from 'vue';
const themeColor = ref('#3b82f6'); // 蓝色主题
provide('globalTheme', themeColor);

然后后代组件用inject接收,接收的时候还可以给个默认值(比如用户没登录或者全局没设置主题的时候用):

// 价格标签组件 PriceTag.vue
import { inject } from 'vue';
// 默认值是橙色,防止后代找不到provide
const globalTheme = inject('globalTheme', ref('#f97316'));

这时候如果在App.vue里修改themeColor.value = '#ec4899',所有后代的globalTheme都会跟着变,完全没问题。

情况2:提供多个reactive响应式数据,或者要传方法

如果要传一堆相关的响应式数据(比如用户信息:用户名、头像、登录时间,还有修改头像的方法),最好用reactive包成一个对象,然后用readonly把整个对象包起来再provide——这不是多此一举,而是官方推荐的最佳实践,因为如果不包readonly,任何后代组件都能直接修改provide的数据,万一某个小同事写的组件不小心改了全局用户信息,排查起来特别困难。

用readonly还有个小技巧:如果要传允许后代调用的方法,不用把方法放在reactive里,因为方法本身不需要响应式,直接单独provide或者和readonly对象一起放在一个provide里都可以。

// App.vue
import { ref, reactive, provide, readonly } from 'vue';
// 用户信息用reactive
const userInfo = reactive({
  name: '',
  avatar: '',
  loginTime: ''
});
// 切换头像的方法,允许后代调用
const changeAvatar = (newAvatarUrl) => {
  userInfo.avatar = newAvatarUrl;
};
// 把整个userInfo变成只读的provide出去,方法单独provide
provide('readonlyUserInfo', readonly(userInfo));
provide('changeAvatar', changeAvatar);

后代组件接收后,只能通过调用changeAvatar来修改头像,直接改readonlyUserInfo.avatar会在控制台报错,安全多了。

Vue3 provide的第二个进阶用法:注入别名、类型保护,适合中大型项目

如果你的项目用的是TypeScript,或者项目里有很多provide/inject,容易出现“key写错了”“不知道inject接收到的是什么类型”的问题,这时候可以用Symbol作为key类型定义文件或者组合式函数包装来解决。

组合式函数包装是更通用的做法,不用TypeScript也能用

为什么要用组合式函数?因为Symbol不能直接在不同组件里写一样的,除非单独定义在一个文件里导出,而组合式函数可以把Symbol的定义、provide的逻辑、inject的逻辑都封装在一起,不仅不会写错key,还能统一管理默认值、类型提示(如果用TS),甚至可以加一些额外的逻辑,比如inject找不到provide的时候给个警告。

比如刚才的全局主题切换,可以封装成一个叫useGlobalTheme的组合式函数,放在composables/useGlobalTheme.js里:

// composables/useGlobalTheme.js
import { ref, provide, inject, readonly } from 'vue';
// 用Symbol作为唯一的key,避免和其他组件的provide key冲突
const GLOBAL_THEME_KEY = Symbol('globalTheme');
const GLOBAL_THEME_CHANGE_KEY = Symbol('globalThemeChange');
// 这个函数是给“提供数据的组件”(比如App.vue)用的
export function provideGlobalTheme(initialTheme = '#3b82f6') {
  const themeColor = ref(initialTheme);
  const changeTheme = (newColor) => {
    themeColor.value = newColor;
  };
  // 把key和数据/方法绑定在一起,别人用组合式函数的inject部分就不会写错
  provide(GLOBAL_THEME_KEY, readonly(themeColor));
  provide(GLOBAL_THEME_CHANGE_KEY, changeTheme);
}
// 这个函数是给“接收数据的组件”用的
export function useGlobalTheme() {
  const themeColor = inject(GLOBAL_THEME_KEY);
  const changeTheme = inject(GLOBAL_THEME_CHANGE_KEY);
  // 加个简单的警告,防止接收组件的父级没有调用provideGlobalTheme
  if (!themeColor || !changeTheme) {
    throw new Error('useGlobalTheme必须在调用了provideGlobalTheme的组件后代中使用!');
  }
  return {
    themeColor,
    changeTheme
  };
}

然后App.vue里直接调用provideGlobalTheme(),不用管key是什么:

// App.vue
import { provideGlobalTheme } from './composables/useGlobalTheme';
// 可以传初始值,也可以不传
provideGlobalTheme('#ec4899');

后代组件里直接useGlobalTheme()

// PriceTag.vue
import { useGlobalTheme } from '../composables/useGlobalTheme';
// 直接解构,不用默认值也不怕,因为组合式函数里有警告
const { themeColor, changeTheme } = useGlobalTheme();

这种写法不仅规范,还能避免很多低级错误,适合中大型团队协作的项目。

Vue3 provide的第三个进阶用法:配合Teleport、Suspense、高阶组件,实现更灵活的功能

很多人以为provide只能在普通的父子嵌套里用,其实它和Vue3的很多新特性都能配合,比如Teleport、Suspense,还有Vue2就有的高阶组件(HOC)。

配合Teleport使用,解决模态框等“飞出去”的组件传参问题

Vue3的Teleport可以把组件的DOM结构“传送”到body或者其他指定的DOM元素里,但组件的逻辑还是属于原来的父组件树的,所以provide/inject依然能用——比如你在一个嵌套很深的按钮组件里用Teleport传了一个模态框,模态框需要用到全局主题色,直接用刚才的useGlobalTheme()就行,完全没问题,不用再通过props或者事件总线。

配合高阶组件使用,实现功能注入

高阶组件简单来说就是“接收一个组件作为参数,返回一个增强后的组件”的函数,Vue3里用组合式函数虽然也能实现很多功能,但有些时候高阶组件配合provide/inject会更方便——比如你要给一批组件都注入“全局主题色”和“修改主题的方法”,不用每个组件都写useGlobalTheme(),可以写一个高阶组件withGlobalTheme

// components/withGlobalTheme.js
import { defineComponent, provideGlobalTheme } from '../composables/useGlobalTheme';
export function withGlobalTheme(WrappedComponent, initialTheme = '#3b82f6') {
  return defineComponent({
    name: `WithGlobalTheme${WrappedComponent.name}`,
    setup(props, { slots }) {
      // 先provide全局主题
      provideGlobalTheme(initialTheme);
      // 然后渲染被包裹的组件
      return () => slots.default();
    }
  });
}

然后直接在App.vue里用这个高阶组件包裹整个应用的内容:

<!-- App.vue -->
<template>
  <WithGlobalTheme>
    <!-- 整个应用的内容,包括侧边栏、商品列表等 -->
    <router-view />
  </WithGlobalTheme>
</template>
<script setup>
import { withGlobalTheme } from './components/withGlobalTheme';
import { RouterView } from 'vue-router';
const WithGlobalTheme = withGlobalTheme(RouterView);
</script>

这样所有路由对应的组件及其后代,都能直接用useGlobalTheme(),不用再在App.vue里单独写provide。

Vue3 provide的常见坑点,踩过一个你可能就要找半天bug

说了这么多好用的用法,接下来必须讲坑点,不然很多人用了之后会踩雷。

坑点1:直接传普通值,或者传ref.value,导致数据不响应

刚才已经提到过,但很多人还是会犯——比如你在App.vue里写const count = ref(0),然后provide('count', count.value),后代组件接收到的就是普通的0,不管你怎么修改count.value,后代都不会更新。一定要传整个ref或reactive对象,或者传readonly包裹的整个对象!

坑点2:没有加readonly,导致后代组件乱改全局数据

这个坑在多人协作的项目里特别常见——比如某个小同事在商品卡片组件里写readonlyUserInfo.avatar = 'xxx',虽然控制台会报错(如果是开发环境),但如果是生产环境没开严格模式,可能不会报错,但数据还是改不了?不对,等一下,开发环境下Vue会给readonly对象的属性设置setter,修改的时候会直接报错;但生产环境下为了性能,Vue可能不会设置setter,修改的时候会静默失败,不管是报错还是静默失败,都会影响项目的稳定性,所以所有全局提供的响应式数据,一定要加readonly! 允许后代修改的话,单独provide一个方法。

坑点3:inject的默认值不能是响应式对象?不对,是默认值如果是对象的话,要注意内存地址

刚才讲基础响应式的时候,给globalTheme的默认值是ref('#f97316'),这是对的,因为每次inject的时候,这个默认值都是新创建的ref吗?不对,等一下,如果直接写inject('globalTheme', ref('#f97316')),那么当多个后代组件都找不到provide的时候,它们接收到的ref是同一个内存地址吗?是的,这就会导致一个后代修改默认值的ref.value,其他后代的默认值也会跟着变,这肯定不是我们想要的。

所以如果inject的默认值是对象或ref/reactive,要把它写成一个返回默认值的函数,这样每次inject的时候,都会创建一个新的内存地址:

// 正确的写法,默认值是函数返回的新ref
const globalTheme = inject('globalTheme', () => ref('#f97316'));

哦对了,这里要注意,Vue3的inject默认值只有当找不到对应的provide的时候才会生效,而且如果默认值是函数的话,不用手动调用,Vue会自动调用它。

坑点4:provide的时机不对,导致后代组件inject不到数据

provide是在组件的setup函数执行的时候才会生效的,所以如果你的后代组件是在setup函数执行之前就挂载的(比如用了Suspense的fallback组件,或者某些异步组件加载的逻辑不对),那么后代组件可能会inject不到数据。

这个坑点的解决方法很简单:尽量在最顶层的组件(比如App.vue)的setup函数里调用provide,这样所有后代组件(包括fallback组件、异步组件)都能inject到数据。

坑点5:不要用provide/inject代替所有的props传参

很多人学了provide/inject之后,觉得它太好用了,不管什么数据都用它传,这是不对的,官方文档里明确说了,provide/inject是为了解决深层嵌套的共享数据问题,对于父子组件之间的直接传参,还是用props和emits更好——因为props和emits更直观,一眼就能看到组件接收什么数据、触发什么事件,方便维护和调试。

比如商品卡片组件需要接收商品的id、名称、价格,这些数据都是从商品列表组件直接传过来的,用props就可以,没必要用provide/inject。

Vue3 provide/inject到底什么时候用?怎么用才规范?

最后给大家总结一下,省得看了这么多忘光了:

  1. 什么时候用? 当数据需要传给3层以上的后代组件,或者需要传给Teleport“飞出去”的组件,或者需要实现全局共享数据(比如主题色、用户登录状态、国际化语言)的时候用。
  2. 怎么用才规范?
    • 尽量用组合式函数封装provide/inject,用Symbol作为唯一的key;
    • 所有全局提供的响应式数据,一定要加readonly;
    • 允许后代修改的话,单独provide一个方法;
    • inject的默认值如果是对象或ref/reactive,要写成函数;
    • 尽量在最顶层的组件里调用provide;
    • 不要用provide/inject代替所有的props传参。

Vue3的provide/inject其实还有很多细节,比如provide的作用域是当前组件的后代,兄弟组件之间不能直接用;比如可以在provide的时候覆盖祖先组件的provide(但最好不要这么做,会导致代码混乱);比如Vue3.3+新增了injectStrict函数,找不到provide的时候会直接报错,不用自己在组合式函数里写警告,不过这些细节今天就不展开讲了,大家可以自己去官方文档里看看,或者在实际开发中慢慢摸索。 就到这里,如果你还有其他关于Vue3的问题,欢迎在评论区留言哦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门