Vue 3 的 `provide`/`inject` 如何实现响应式传递?它与 Vue 2 的 `provide`/`inject` 有何区别?

咳咳,各位观众老爷,晚上好!欢迎来到“Vue响应式宇宙漫游指南”讲座。今天咱们就来聊聊 Vue 3 的 provide/inject 这对好基友,看看它们是怎么在组件树里穿梭,把数据像快递一样安全送达的。尤其是它们在 Vue 3 里怎么变得更“懂事”了,能响应式传递数据,告别了 Vue 2 时代的某些小脾气。

Part 1: provide/inject 是个啥?

想象一下,你有一个庞大的家族,老爸(根组件)想给孙子(深层组件)送个礼物(数据),但是老爸跟孙子之间隔着N多辈人。如果让老爸挨个问:“儿子啊,你帮我把这个给你的儿子,让他再给他儿子…”,那不得累死?

provide/inject 就是解决这个问题的。老爸直接把礼物放到一个公共的“快递柜”(provide),孙子直接去快递柜取(inject)。中间的儿子们根本不需要知道有这事儿,也不需要帮忙转发。

简单来说:

  • provide: 在父组件中声明一个变量或者一个对象,并将其提供给后代组件。
  • inject: 在后代组件中声明要接收的变量或者对象,并从父组件提供的 provide 中获取。

Part 2: Vue 2 的 provide/inject: “老式快递,有点慢”

在 Vue 2 中,provide 可以是:

  • 一个对象:提供静态数据。
  • 一个返回对象的函数:提供动态数据,但不是响应式的

inject 接收的是 provide 提供的值,但是,重点来了:Vue 2 的 provide/inject 默认情况下不是响应式的!

啥意思?就是说,如果 provide 里的数据变了,inject 的组件不会自动更新。你需要手动触发更新,比如通过事件总线或者 Vuex。

// Vue 2 - 父组件
export default {
  provide: {
    message: 'Hello from parent!' // 静态数据
  },
  data() {
    return {
      count: 0
    }
  },
  provide() { //动态数据,但不是响应式
    return {
      count: this.count
    }
  },
  template: `
    <div>
      <button @click="count++">Increment</button>
    </div>
  `
}

// Vue 2 - 子组件
export default {
  inject: ['message', 'count'],
  mounted() {
    console.log(this.message); // "Hello from parent!"
    console.log(this.count); // 0
  },
  template: `
    <div>
      <p>{{ message }}</p>
      <p>Count: {{ count }}</p>
    </div>
  `
}

在这个例子里,点击按钮增加 count,父组件的 count 会改变,但是子组件的 count 不会更新,因为它不是响应式的。

手动更新的常见方法 (Vue 2)

  • 使用事件总线: 父组件在 count 改变时触发事件,子组件监听事件并更新自己的数据。
  • 使用 Vuex:count 放到 Vuex store 中,父组件和子组件都从 store 中获取 count,store 的数据改变会自动触发组件更新。

总结:Vue 2 的 provide/inject 主要问题:

  • 非响应式: provide 里的数据变化不会自动更新 inject 的组件。
  • 维护成本高: 需要手动处理更新,增加代码复杂度和维护成本。
特性 Vue 2
响应性 默认非响应式,需要手动处理更新
数据类型 可以是对象或返回对象的函数
使用场景 传递静态数据,或者结合其他手段实现动态数据传递
维护成本 较高,需要手动处理更新,增加代码复杂度和维护成本

Part 3: Vue 3 的 provide/inject: “智能快递,速度快,还保鲜!”

Vue 3 对 provide/inject 进行了重大升级,让它真正实现了响应式数据传递。 这得益于 Vue 3 的响应式系统,它基于 Proxy,可以更精细地追踪数据的变化。

3.1 Composition API 中的 provide/inject

在 Vue 3 的 Composition API 中,provideinject 变成了两个函数。

  • provide(key, value): 接收两个参数,key 是一个字符串或者 Symbol,用于标识提供的数据,value 是要提供的数据。
  • inject(key, defaultValue): 接收两个参数,key 是一个字符串或者 Symbol,与 provide 里的 key 对应,defaultValue 是一个可选的默认值,如果找不到对应的 provide,就使用这个默认值。

关键:value 可以是任何响应式数据! 例如 refreactive 创建的数据,甚至是 computed 计算属性。

// Vue 3 - 父组件 (使用 Composition API)
<template>
  <div>
    <button @click="count++">Increment</button>
    <p>Parent Count: {{ count }}</p>
  </div>
</template>

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

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

    provide('count', count); // 提供响应式数据

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

// Vue 3 - 子组件 (使用 Composition API)
<template>
  <div>
    <p>Child Count: {{ injectedCount }}</p>
  </div>
</template>

<script>
import { inject, ref, onMounted } from 'vue';

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

    onMounted(() => {
      console.log("injectCount", injectedCount.value)
    })

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

在这个例子中,父组件使用 ref 创建了一个响应式的 count,并使用 provide 将其提供给后代组件。子组件使用 inject 接收 count,并将其赋值给 injectedCount。当父组件的 count 改变时,子组件的 injectedCount 会自动更新! 这就是响应式的力量!

3.2 Options API 中的 provide/inject (兼容写法)

Vue 3 也兼容 Options API 的 provide/inject 写法,但内部实现有所不同,它会利用 Vue 3 的响应式系统来确保数据的响应性。

// Vue 3 - 父组件 (使用 Options API)
<template>
  <div>
    <button @click="count++">Increment</button>
    <p>Parent Count: {{ count }}</p>
  </div>
</template>

<script>
import { ref, reactive } from 'vue';

export default {
  data() {
    return {
      count: ref(0), // 使用 ref 创建响应式数据
      // anotherData: reactive({ // 使用 reactive 创建响应式数据
      //   value: "initial"
      // })
    }
  },
  provide() {
    return {
      count: this.count, // 提供响应式数据
      // anotherData: this.anotherData
    };
  },
};
</script>

// Vue 3 - 子组件 (使用 Options API)
<template>
  <div>
    <p>Child Count: {{ count.value }}</p>
    <!-- <p>Another Data: {{ anotherData.value }}</p> -->
  </div>
</template>

<script>
export default {
  inject: ['count', 'anotherData'],
  mounted() {
    console.log("count", this.count.value)
    // console.log("anotherData", this.anotherData.value)
  }
};
</script>

注意:在使用 Options API 的 provide/inject 时,确保 provide 提供的是响应式数据(例如 refreactive 创建的数据),否则仍然无法实现响应式更新。

3.3 使用 Symbol 作为 Key

为了避免命名冲突,可以使用 Symbol 作为 provide/inject 的 key。

// 定义一个 Symbol
const myKey = Symbol('myKey');

// 父组件
provide(myKey, ref('Hello from parent!'));

// 子组件
const injectedValue = inject(myKey);

使用 Symbol 可以确保 key 的唯一性,避免在大型项目中出现命名冲突的问题。

Part 4: provide/inject 的高级用法

  • 默认值: inject 可以接收一个默认值,当父组件没有提供对应的 provide 时,子组件会使用这个默认值。

    // 子组件
    const injectedValue = inject('myKey', 'Default Value');
  • 响应式默认值: 默认值也可以是响应式的。

    // 子组件
    import { ref } from 'vue';
    
    const defaultValue = ref('Default Value');
    const injectedValue = inject('myKey', defaultValue);
  • 转换注入的值: 可以使用 computed 来转换注入的值。

    // 子组件
    import { inject, computed } from 'vue';
    
    const rawValue = inject('myKey');
    const formattedValue = computed(() => rawValue.value.toUpperCase());
  • provide 函数中使用 this
    在 Options API 中,provide 可以是一个函数,允许你访问组件实例的 this。这在需要基于组件内部状态提供数据时非常有用。

    // 父组件
    <template>
     <div>
       <button @click="increment">Increment</button>
       <p>Count: {{ count }}</p>
     </div>
    </template>
    
    <script>
    import { ref } from 'vue';
    
    export default {
     data() {
       return {
         count: ref(0)
       };
     },
     methods: {
       increment() {
         this.count++;
       }
     },
     provide() {
       return {
         getCount: () => this.count.value, // 提供一个获取 count 值的函数
         incrementCount: this.increment // 提供一个递增 count 值的函数
       };
     }
    };
    </script>
    
    // 子组件
    <template>
     <div>
       <p>Count from parent: {{ getCount() }}</p>
       <button @click="incrementCount">Increment in parent</button>
     </div>
    </template>
    
    <script>
    import { inject } from 'vue';
    
    export default {
     setup() {
       const getCount = inject('getCount');
       const incrementCount = inject('incrementCount');
    
       return {
         getCount,
         incrementCount
       };
     }
    };
    </script>

    在这个例子中,父组件提供了一个 getCount 函数和一个 incrementCount 函数。子组件可以通过调用这些函数来获取父组件的 count 值,甚至可以直接在子组件中修改父组件的 count 值。

Part 5: Vue 2 vs Vue 3: 总结一下区别

特性 Vue 2 Vue 3
响应性 默认非响应式,需要手动处理更新 默认响应式,provide 里的数据变化会自动更新 inject 的组件
数据类型 可以是对象或返回对象的函数 可以是任何响应式数据 (例如 ref, reactive, computed)
API 对象形式 Composition API: provide(key, value), inject(key, defaultValue)
使用场景 传递静态数据,或者结合其他手段实现动态数据传递 传递动态数据,特别是在深层组件之间共享状态
维护成本 较高,需要手动处理更新,增加代码复杂度和维护成本 较低,响应式更新减少了手动管理状态的需要
TypeScript 支持 较弱 更好,Composition API 更容易进行类型推断
Key 类型 字符串或 Symbol 字符串或 Symbol
默认值 不直接支持,需要手动实现 inject 函数支持默认值参数

Part 6: 使用 provide/inject 的注意事项

  • 避免过度使用: provide/inject 适合在深层组件之间共享数据,如果组件层级不深,或者只需要在父子组件之间传递数据,建议使用 propsemits
  • 命名空间: 使用 Symbol 作为 key 可以避免命名冲突,尤其是在大型项目中。
  • 单向数据流: 虽然 provide/inject 可以传递响应式数据,但仍然要遵循单向数据流的原则,不要在子组件中直接修改父组件提供的数据,而是通过 emit 触发事件,让父组件来修改数据。
  • 可维护性: provide/inject 可能会使组件之间的依赖关系变得隐式,降低代码的可读性和可维护性。建议添加注释,说明哪些组件提供了哪些数据,哪些组件使用了哪些数据。
  • 测试: provide/inject 可能会增加测试的难度,因为组件之间的依赖关系是隐式的。建议编写单元测试,确保组件能够正确地提供和接收数据。

Part 7: 总结

Vue 3 的 provide/inject 是一项强大的功能,它允许我们在组件树中方便地传递响应式数据。相比 Vue 2,Vue 3 的 provide/inject 更加智能、高效,减少了手动管理状态的需要,提高了开发效率。 但是,在使用 provide/inject 时,也要注意一些潜在的问题,例如过度使用、命名冲突、可维护性等。 合理地使用 provide/inject,可以使我们的代码更加简洁、优雅。

好了,今天的“Vue响应式宇宙漫游指南”讲座就到这里。希望大家对 Vue 3 的 provide/inject 有了更深入的了解。 感谢各位的观看,下次再见!

发表回复

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