一、引言
在 Vue.js 的组件通信机制中,provide 和 inject 是一对非常有用的选项,它们允许祖先组件向其所有子孙组件提供数据和方法,实现了一种跨组件层级的依赖注入方式。本文不仅会介绍 provide 和 inject 的基本用法,还会深入探讨它们的响应式特性,以及这些特性背后的技术原理。
Vue 的核心理念之一是组件化开发,即把应用分割成多个独立且可复用的组件。然而,在复杂的组件树结构中,不同层次间的组件通信可能变得棘手。虽然可以使用 props 向下传递数据,也可以通过事件向上发送消息,但当涉及多层嵌套时,这样的做法会使代码变得冗长且难以维护。因此,Vue 提供了 provide 和 inject 这两个工具来简化这种情形下的组件间通信。它们为开发者提供了一种灵活的方式,可以在不破坏现有组件结构的情况下,轻松地共享状态或行为。
二、inject/provide 基础回顾
Provide
在 Vue 组件中,provide 是一个函数,它返回一个对象,这个对象包含了要提供给子孙组件的数据和方法。例如:
// 祖先组件
export default {
provide() {
return {
message: 'msg',
getMessage: () => 'This is a provided message.'
};
}
}
Inject
子孙组件可以通过 inject 来接收祖先组件提供的数据和方法。inject 可以是一个字符串数组或一个对象。以下是使用字符串数组的示例:
// 子孙组件
export default {
inject: ['message', 'getMessage'],
mounted() {
console.log(this.message); // 输出: msg
console.log(this.getMessage()); // 输出: This is a provided message.
}
}
三、inject/provide 的响应式原理
默认情况下,基础类型(如字符串、数字、布尔值)是不会有响应式的。这意味着如果你从 provide 中提供了一个基础类型的值,那么即使该值在其原始位置发生了变化,后代组件也不会自动更新。例如:
// 父
<template>
<div>
<HelloWorld />
{{ msg }}
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
components: { HelloWorld },
data() {
return {
msg: "Hello World",
};
},
provide() {
return {
message: this.msg,
updateMessage: () => {
this.msg = "Hello Vue";
},
};
},
};
</script>
// 子
<template>
<div @click="updateMessage">
{{ message }}
</div>
</template>
<script>
export default {
name: "HelloWorld",
inject: ["message", "updateMessage"],
};
</script>
在这种情况下,message 在子孙组件中不会响应地更新,因为它是直接从 this.msg 赋值的一个新副本,失去了与原始数据的联系。为了让 provide 提供的数据具有响应性,我们需要提供整个响应式对象,而不是它的属性。这样做的原因在于,Vue 需要能够追踪到对象内部属性的变化,以便及时通知相关视图进行更新。对于基础类型的值,由于它们是不可变的,一旦被复制就不会再有改变,所以无法实现响应式更新。
// 父
<template>
<div>
<HelloWorld />
{{ messageState.msg }}
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
components: { HelloWorld },
data() {
return {
messageState: {
msg: "Hello World",
},
};
},
provide() {
// 提供整个响应式对象,而不是它的属性
return {
messageState: this.messageState,
updateMessage: () => {
this.messageState.msg = "Hello Vue";
},
};
},
};
</script>
// 子
<template>
<div @click="updateMessage">
{{ messageState.msg }}
</div>
</template>
<script>
export default {
name: "HelloWorld",
inject: ["messageState", "updateMessage"],
};
</script>
- 为何基础类型不具响应性? 当你提供一个基础类型的值(如字符串、数字等),Vue 会创建该值的一个副本,并将其提供给子孙组件。因为基础类型是按值传递的,所以子孙组件接收到的是一个完全独立的副本,任何对原始值的更改都不会影响到副本,反之亦然。这就是为什么基础类型的值不具备响应性的原因。
- 如何实现响应性? 为了确保提供的数据在更新时能够触发视图的重新渲染,我们应该提供一个引用类型(如对象或数组)。Vue 会对这些引用类型进行代理,使其变为响应式的。当你更新对象中的某个属性时,Vue 能够检测到变化并通知相关的视图进行更新。这保证了无论在哪一层级的组件中,只要该属性发生变化,所有使用该属性的地方都会得到最新的值。
- Vue如何实现响应性? 在 Vue 2 中,Vue 使用
Object.defineProperty()来拦截对对象属性的访问和修改,从而实现了响应式系统。每当属性被读取或设置时,Vue 都能知道,并据此触发相应的视图更新。而在 Vue 3 中,Vue 则利用了 ES6 的Proxy对象,提供了更强大的功能和更好的性能,尤其是在处理深层嵌套对象时。Proxy允许我们定义自定义的行为,比如拦截对象的操作,而无需修改原对象本身。
四、Vue 3 中的变化
在 Vue 3 中,默认情况下 provide 和 inject 的值也是非响应式的,但 Vue 3 引入了 reactive 等响应式 API,使得创建响应式对象更加直观。例如:
// 父组件 App.vue
<template>
<div>
<h1>{{ state.msg }}</h1>
<button @click="state.changeMes">修改消息</button>
<ChildComponent />
</div>
</template>
<script setup>
import { provide, reactive } from "vue";
import ChildComponent from "./components/ChildComponent.vue";
// 创建包含消息和修改消息函数的响应式对象
const state = reactive({
msg: "Hello World",
changeMes: () => {
state.msg = "Hello Vue";
},
});
// 提供这个响应式对象给子组件
provide("mesObj", state);
</script>
//子组件 ChildComponent
<template>
<div>
<p>子组件中显示的消息: {{ state.msg }}</p>
</div>
</template>
<script setup>
import { inject } from "vue";
// 注入父组件提供的响应式对象
const state = inject("mesObj");
</script>
这与 Vue 2 的逻辑相同,只是使用了新的响应式 API。Vue 3 利用了 ES6 的 Proxy 对象,它比 Object.defineProperty 提供了更强大的功能和更好的性能,尤其是在处理深层嵌套对象时。此外,Vue 3 的 Composition API 也鼓励开发者将状态管理逻辑与模板分离,从而提高了代码的可读性和可维护性。
关于CompositionAPI:
Composition API 是 Vue 3 中引入的新特性,它提供了一种更灵活的方式来组织和重用逻辑。与 Options API 不同,Composition API 允许我们将逻辑关注点分解成较小的函数,并根据需要组合它们。这对于大型应用程序特别有用,因为它可以帮助保持代码的整洁和模块化。使用 Composition API 创建的响应式对象可以直接用于 provide,并且这些对象会在所有使用 inject 接收它们的组件之间保持同步
五、inject/provide 在项目中的常用场景
1. 直接传递父组件实例:
// 父组件
provide() {
return {
main: this
};
}
// 子组件
export default {
inject: ['main'],
mounted() {
console.log('在子孙组件中获取到祖先组件实例,可以访问其数据和方法,并且具有响应式');
console.log(this.main);
}
};
2. 通用方法和属性:
// 父组件
provide() {
return {
goWorkDetail: () => this.$router.push('/detail'),
TYPE: 'add'
};
}
// 子组件
export default {
inject: ['TYPE','goWorkDetail'],
mounted() {
this.goWorkDetail()
console.log(this.TYPE);
}
};
3. 利用事件总线实现同级通信:
创建文件 src/eventBus.js:
// src/eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
编辑 src/App.vue 文件,使其作为祖先组件,提供购物车状态和方法:
<!-- src/App.vue -->
<template>
<div id="app">
<h1>Shopping Cart</h1>
<ProductList />
<CartSummary />
</div>
</template>
<script>
import ProductList from './components/ProductList.vue';
import CartSummary from './components/CartSummary.vue';
import { EventBus } from './eventBus';
export default {
name: 'App',
components: {
ProductList,
CartSummary
},
data() {
return {
cart: []
};
},
provide() {
return {
addToCart: this.addToCart,
cartItems: this.cart,
eventBus: EventBus // 提供事件总线
};
},
methods: {
addToCart(product) {
this.cart.push(product);
EventBus.$emit('cart-updated', this.cart.length); // 触发事件通知购物车更新
}
}
};
</script>
创建文件 src/components/ProductList.vue,用于显示商品列表并允许用户添加商品到购物车:
<!-- src/components/ProductList.vue -->
<template>
<div class="product-list">
<h2>Products</h2>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} ({{ product.price }})
<button @click="addToCart(product)">Add to Cart</button>
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['addToCart'], // 注入 addToCart 方法
data() {
return {
products: [
{ id: 1, name: '黑神话悟空', price: '256' },
{ id: 2, name: '荒野大嫖客', price: '298' },
{ id: 3, name: '英雄联盟', price: '0' }
]
};
}
};
</script>
创建文件 src/components/CartSummary.vue,用于显示购物车摘要,并监听购物车更新事件:
<!-- src/components/CartSummary.vue -->
<template>
<div class="cart-summary">
<h2>Cart Summary</h2>
<p>Total Items: {{ totalItems }}</p>
</div>
</template>
<script>
export default {
inject: ['cartItems', 'eventBus'], // 注入 cartItems 和 eventBus
data() {
return {
totalItems: 0
};
},
created() {
this.eventBus.$on('cart-updated', (count) => {
this.totalItems = count;
});
},
beforeDestroy() {
this.eventBus.$off('cart-updated'); // 移除事件监听器,防止内存泄漏
}
};
</script>
关于事件总线:
事件总线是一种设计模式,它允许组件之间通过发布/订阅模型来进行解耦通信。在这个例子中,EventBus 实际上就是一个 Vue 实例,它充当了所有组件之间的中间人。当某个组件想要通知其他组件发生了一些事情时,它可以向 EventBus 发布一个事件;而那些对该事件感兴趣的组件则可以订阅该事件,并执行相应的动作。这种方法尤其适合用于兄弟组件之间的通信,或是当不想让组件之间直接相互依赖的时候。不过需要注意的是,过度使用事件总线可能会导致代码难以跟踪和调试,因此应该谨慎使用,并尽量保持事件的数量和复杂度在可控范围内。
总结
可以看到,使用引用类型才能让 provide 和 inject 具有响应式,具体原因是因为只有代理过的属性才能具有响应式。这是因为 Vue 使用了特定的机制来追踪对象内部的变化,而基础类型的值一旦被复制就不再受此机制的影响。因此,当我们希望所提供的数据能够在子孙组件之间保持同步时,我们应该确保提供的数据是一个响应式对象,而不是简单地复制基础类型的值。这样做不仅能保证数据的一致性,还能提高应用的性能和用户体验。