前言
在 Vue 3 中,组件间通信是构建可维护且灵活的应用程序的关键。随着应用程序变得越来越复杂,组件之间有效地共享数据和状态变得至关重要。Vue 3 提供了多种机制来实现这一点,从简单的父子组件通信到更为复杂的跨组件通信。
父子组件通信 defineProps
在 Vue 3 中,父组件向子组件传递数据通常通过 props 实现。使用 defineProps 选项来声明子组件接收的 props。 父组件-
<template> <Child :message="message"/></template>
<script setup> import Child from './child.vue' import { ref } from 'vue'const message=ref('父组件数据')</script>子组件-
<template> <div> <p>{{message}}</p> </div></template>
<script setup>import { defineProps } from 'vue';const props=defineProps({ message: { type: String, required: true }})</script>修改父组件数据
当我们希望子组件能够修改父组件的数据时,有下面两种思路-
传递修改函数(Prop + Callback)
<template> <Child :message="message" :updateMessage="updateMessage"/> //传递修改方法</template>
<script setup> import Child from './child.vue' import { ref } from 'vue'const message=ref('父类的数据')function updateMessage() { message.value = '修改后父类的数据';}</script>子组件-
<template> <div> <p>{{message}}</p> <button @click="updateMessage">Change</button> </div></template>
<script setup>import { defineProps } from 'vue';const props=defineProps({ message: { type: String, required: true }, updateMessage:{ type: Function, required: true }})</script>子组件修改后再通知父组件(Event Emitter)
子组件能够修改父组件的数据时,可以通过定义一个自定义事件并在父组件中监听该事件来实现这一目的。 子组件-
<template> <div> <p>{{message}}</p> <button @click="updateMessage">Change</button> </div></template>
<script setup>import { defineProps , defineEmits } from 'vue';const props=defineProps({ message: { type: String, required: true },})// 定义一个自定义事件 update,并在父组件中通过 v-on:update 接收并处理const emits = defineEmits(['update']); //定义事件const updateMessage = () => { emits('update', '修改后父类的数据'); //发布事件}</script>父组件-
<template> <Child :message="message" @update="changeMsg"/> //监听(订阅)事件</template>
<script setup> import Child from './child.vue' import { ref } from 'vue'const message=ref('父组件数据')
function changeMsg(value) { message.value = value;}</script>v-model绑定更新
也可以使用v-model绑定子组件。父组件通过 v-model 绑定一个属性到子组件,而子组件则通过触发一个特定的事件来通知父组件数据已经更新。这里的关键在于子组件需要触发一个名为 update:modelValue 的事件,这样 Vue 才能识别这是一个 v-model 的更新事件,并自动更新父组件中的数据。
<template> <Child :message="message" v-model="message"/></template>
<script setup> import Child from './child.vue' import { ref } from 'vue'const message=ref('父类的数据')</script>子组件-
<template> <div> <p>{{message}}</p> <button @click="updateMessage">Change</button> </div></template>
<script setup>import { defineProps , defineEmits } from 'vue';const props=defineProps({ message: { type: String, required: true },})
const emits = defineEmits(['update:modelValue']);const updateMessage = () => { emits('update:modelValue', '修改后父类的数据');}</script>如果想要自己使用自定义事件名称,需要在 v-model 中添加一个 :event 修饰符,并指定你想要使用的事件名称。 父组件-
<Child :message="message" v-model:changeMsg="message"/>子组件-
const emits = defineEmits(['update:changeMsg']);const updateMessage = () => { emits('update:changeMsg', '修改后父类的数据');}子父通信 defineEmits
当我们希望子组件能够将数据传递给父组件时,通常的做法是通过子组件触发事件使用 defineEmits来通知父组件。这里我们切记使用defineExpose暴露给父组件的数据是不会在父组件里面实时更新的 子组件-
<template><div> <h2>子组件</h2> <p>消息-{{ message }}</p> <button @click="updateMessage">change</button></div></template>
<script setup> import { ref,defineExpose } from 'vue' const message=ref('子类的数据') function updateMessage() { message.value = '子组件的消息已更改';}defineExpose({ message, updateMessage });</script>父组件-
<template> <Child ref="childRef" /> <div> <h2>父组件</h2> <p>消息-{{parentMessage }}</p> <button @click="updateFromChild">childeMsg</button></div></template>
<script setup> import Child from './child.vue' import { ref } from 'vue' const parentMessage = ref('父组件的初始消息'); const childRef = ref(null); function updateFromChild() { parentMessage.value = childRef.value.message;}</script>跨级通信 provide && inject
provide 和 inject 是一种在组件树中进行跨层级通信的方式。它们允许你将数据从祖先组件传递给其后代组件,而无需通过中间组件逐层传递 props。这种方式特别适合于那些需要被多个组件访问的数据或方法,而且这些组件之间没有直接的父子关系。
provide/inject比较简单,声明provide(键名, 键值),注入-inject(键名)返回出键值
顶级组件-
<template> <div> <h1>Top Component</h1> <middle-component /> </div></template><script setup>import MiddleComponent from './Middle.vue';const topData = 'Data from the top component';provide('topData', topData);</script>中间组件-
<template> <div> <h2>Middle Component</h2> <leaf-component /> </div></template><script setup>import LeafComponent from './Leaf.vue';</script>叶子组件-
<template> <div> <h3>Leaf Component</h3> <p>Data from top: {{ topData }}</p> </div></template>
<script setup>import { inject } from 'vue';const topData = inject('topData');</script>动态路由通信
我们还可以利用路由来实现不同组件间的通信。
Params作为参数
Params 参数是通过路径的一部分来传递的,通常用于表示动态的部分,例如 /users/:userId 中的 :userId 就是一个 param 参数。
import { createWebHistory,createRouter } from "vue-router"
const routes = [{path: "/parent",name: "parent",component: () => import("@/components/demo2/parent.vue"),},{path: "/child/:id", //传递的参数name: "child", // 命名路由component: () => import("@/components/demo2/child.vue"),}];
const router = createRouter({ history: createWebHistory(), routes, });
export default router;`<template> <h1>父组件</h1> <button @click="goSon">点击跳转</button></template><script setup>import {useRouter} from 'vue-router'const router = useRouter()const goSon=()=>{router.push({name:'child',params:{id: '123123'}})}</script><template>son: route.params = {{ route.params.id }}</template><script setup>import {useRoute}from 'vue-router'const route = useRoute() //使用useRoute获取当前页面信息</script>当用户访问类似 /child/123 这样的 URL 时,:id 的值会被自动提取出来,并且可以在这个组件中通过 route.params.id 获取到。而当我们如果需要从一个组件导航到另一个带有参数的路由,可以使用 router.push 方法,并且提供参数的值,从而实现传值。
Query 参数
Query 参数则是通过 URL 中的查询字符串来传递的,通常用于过滤、排序或者传递额外的信息,例如 /users?name=John 中的 name=John 就是一个 query 参数。
import { createWebHistory,createRouter } from "vue-router";const routes = [{path: "/parent",component: () => import("@/components/demo2/parent.vue"),},{path: "/child", //query传值不需要使用name和修改path格式component: () => import("@/components/demo2/child.vue"),}];const router = createRouter({ history: createWebHistory(), routes, });export default router;<template><h1>父组件</h1><button @click="goSon">点击跳转</button></template><script setup>import {useRouter} from 'vue-router'const router = useRouter()const goSon=()=>{router.push({path:'/child',query:{id: '123123'}})}</script><template>son: route.query.id = {{ route.query.id }}</template><script setup>import {useRoute}from 'vue-router' const route = useRoute()</script>使用query传参时,不需要我们在路由文件中修改目标页面的path路径而格式,而是在当前页面使用router.push(path:'xxx',query:{键名-键值}),在跳转到目标页面后,目标页面的url会在后面加上xxx?键名=键值。而在目标页面要取到这个值,也是使用useRoute获取到当前页面信息使用route.query.id得到对应的query。
teleport
使用 Teleport 组件的主要目的是为了能够将一个组件渲染到 DOM 树中的另一个位置,而不是直接在其父组件的 DOM 子树内。这在处理模态框、弹窗等需要跨越多个组件层级的 UI 元素时特别有用。
非常好的例子就是登录弹窗的动态使用Teleport来实现的,如果我们使用的是登入弹窗,那么不论在哪个页面,这个弹窗框的层级应该是最高的,同时应该脱离文档流不影响其下面的结构。我们在HTML中会使用Z-index属性,但是在vue组件化结构中是不生效的,因此Teleport便孕育而生。
index.html文件-
<div id="app"> <!-- 应用的主要内容 --> <App></App> </div>
<!-- Teleport 的目标容器 --> <div id="login-container"></div>App.vue文件-
<template> <div> <button @click="showLogin = true">Login</button> 这是之外的数据 <teleport v-if="showLogin" to="#login-container"> <div class="login-modal"> <h1>登入</h1> <input type="text" placeholder="Username" v-model="username"> <input type="password" placeholder="Password" v-model="password"> <button @click="submitLogin">提交</button> </div> </teleport> </div></template>
<script setup>import{ref}from 'vue' const showLogin=ref(false) const username=ref('') const password=ref('') const submitLogin=()=>{ //登入逻辑 showLogin.value = false; // Close the modal }</script>为什么需要在#app内再放一个<App />?
- 分离关注点:
<App>元素允许我们在 HTML 文件中定义应用程序的主要内容区域,而main.js中的app.mount('#app')则负责将 Vue 组件挂载到这个区域。
- 保持 HTML 结构清晰:
- 通过在 HTML 中定义
<App>,我们可以保留 HTML 文件的基本结构,使其易于阅读和维护。
- 通过在 HTML 中定义
换句话说我们也可以在#app内再放一个<App />但是这样做使得我们的登入的DOM结构不再清晰,这时不被优秀的程序设计所接受的。
Pinia
Pinia针对Vue 3设计,充分利用Composition API,提倡更简洁和直观的状态管理方式。每个store在Pinia中都是独立的,同时为了兼容Vuex的写法Pinia仍然支持使用选项式API,也是现在主流的状态管理库。
在main.js文件中-
import { createApp } from 'vue'import App from './App.vue'import { createPinia } from 'pinia'
const pinia=createPinia() //创建pinia实例对象createApp(App).use(pinia) // use这个实例对象.mount('#app')在store文件中
import { defineStore } from "pinia";import { ref, computed } from 'vue';export const useCounterStore = defineStore('counter', () => { // state const count = ref(0); // actions const increment = () => { count.value++; }; const decrement = () => { count.value--; }; const reset = () => { count.value = 0; }; const doubleCount = computed(() => { return count.value * 2; }); return { count, increment, decrement, reset, doubleCount }; },);export const useStore = defineStore('store', () => { const someState=ref('hello Pinia'); const incrementSomeState = () => { someState.value += '!' } return { someState , incrementSomeState } },);child.vue文件-
<template> <div> <h1>App</h1> <h1>Counter: {{ store.count }}</h1> <h1>Double:{{ store.doubleCount }}</h1> <button @click="store.increment">Increment</button> <button @click="store.decrement">decrement</button> <button @click="store.reset">reset</button> </div> <h1>String: {{ hel.someState }}</h1> <button @click="hel.incrementSomeState">+ ! </button> <Child /></template><script setup>import {useCounterStore,useStore} from '@/Pinia/counter.js' //引入定义好的状态库import Child from '@/components/child.vue'const store=useCounterStore()const hel=useStore()</script>这里在pinia里我们使用的是Composition API 的方式书写,在store文件中我们将 state,action,getter统统return出交给引入的组件使用,当组件调用这些API时,store将会记录内部的状态并修改,从而响应式的将所有引入的该状态的组件值修改。
Vuex
Vuex设计为一个全局状态管理的单例模式,有一个中心化的存储来管理应用的所有状态。它基于Vue 2的Options API,强调状态的集中管理和严格的规则,包括State、Getters、Mutations(用于同步更改状态)和Actions(可以处理异步逻辑)。其使用与pinia的CompositionAPI 的写法还是略有不同的。 main.js文件-
import { createApp } from 'vue'import './style.css'import App from './App.vue'import store from './store' // vuex是导入状态仓库,pinia是创建实例对象createPinia
const app=createApp(App)app.use(store)app.mount('#app')store文件内-
import { createStore } from 'vuex'const state = { counter: 0,//计数状态 user: null}const actions = { //修改状态的方法 increment: ({ commit }) => { // commit 提交一项修改 提交给mutations commit('increment') },}//所有的状态修改都要经过mutations,只有mutations 可以修改状态const mutations = { increment(state) { state.counter++ }}//getterconst getters = { doubleCount: (state) => { return state.counter * 2 }}//除了读操作,对写操作十分严格const store = createStore({state,actions,mutations,getters}) //创建实例对象export default storechild.vue组件
<template> <h2>App.vue:</h2> <div> count: {{ store.state.counter }} </div> <button @click="increment">+</button> <div> doubleCount: {{store.getters.doubleCount}} </div> <AppHeader /></template>
<script setup>import {useStore} from 'vuex'import AppHeader from './components/app-header.vue'const store=useStore()const increment = () => { //使用dispatch派出increment action的方法 store.dispatch('increment')}</script>注-
- 在
main.js文件中vuex使用的是导出的store仓库,而pinia是使用实例化对象 store仓库中的actions是提交修改方法名,而真正修改是通过mutations,actions就像是秘书,而mutation像是boss,你不能直接找到boss修改属性,而必须通过dispatch派出increment action的方法,让mutation去修改这个值。- vuex中的
getter是不需要引入computed这个方法的,他会自动一直监听内部的状态是否发生改变。
Slot 插槽
slot槽提供了一种灵活的方式来组织和重用组件结构,同时也支持条件渲染和动态内容。它的大概的工作原理如下,就是在组件标签内部,放入父组件数据,用来替代子组件需要接受父组件的渲染数据。
基础使用
slot基础使用在父组件中非常简单,只需要在组件的自闭合标签内放入传递的信息即可,而在子组件中放置<slot />的位置将被父组件传递过来的数据代替。
父组件-
<template><h1 style="color: brown;">父组件</h1><button @click="change">change</button> <Demo4> {{ msg }} </Demo4></template><script setup>import Demo4 from "./components/demo4/child.vue";import {ref}from 'vue'const msg=ref('父组件初始信息')const change=()=>{ msg.value='父组件改变后的信息'}</script>子组件-
<template><div> <h2>子组件</h2> <slot /></div></template>这里在实验中我们顺带检测了一下父组件让子组件展示的数据是不是跟随着父组件的数据改变而实时更新的,可以看到当父组件的数据发生改变时,子组件展示的数据也发生了改变。
具名插槽
具名插槽是 Vue 中一个非常有用的特性,它允许我们在一个组件的不同位置定义多个插槽,并给这些插槽命名。这使得你可以更精确地控制子组件内部的内容布局。具名插槽允许父组件向子组件的不同位置插入内容。这可以通过在子组件模板中定义具名的 <slot> 元素来实现,并通过 name 属性指定插槽的名称。
父组件-
<template> <h1>这是app文件</h1> <Demo4 class="role-select-background1" @click="router.push('/donor')"> <template #title> 这是使用 # </template> <template #desc> 具名slot 的使用 </template> </Demo4>
<Demo4 class="role-select-background2" @click="router.push('/donor')"> <template v-slot:title> 这是使用 v-slot </template> <template v-slot:desc> 具名slot 的使用 </template> </Demo4></template>子组件-
<template> <div class="role-select"> <div class="title"> <slot name="title"/> </div> <div class="desc"> <slot name="desc" /> </div></div></template>这里我们展示了两种常见的具名插槽的用法,当我们使用具名插槽时,<template>内可以使用v-slot:name或者#name来命名插槽。如果父组件没有为某个具名插槽提供内容,那么默认内容(如果有的话)将会被显示。
本地浏览器缓存
使用浏览器的本地存储(如 localStorage 或 sessionStorage)来进行跨组件或跨页面的数据共享。这在 Vue.js 应用中是一种常见的做法,尤其是在需要持久化数据或在不同组件之间共享数据时。
浏览器提供了两种主要的本地存储机制-
localStorage: 数据会永久保存,除非用户手动清除或者代码中明确清除。sessionStorage: 数据仅在当前浏览器会话期间可用,当浏览器窗口关闭时数据会被清除。
<template> <div> 父组件: </div> <button @click="updata">上传数据到Storage</button> <Child /></template>
<script setup>import Child from './components/demo4/child.vue';import { ref } from 'vue';
const data1=ref('父组件的数据localStorageData')const data2=ref('父组件的数据sessionStorageData')
const updata=()=>{ localStorage.setItem('localApp',data1.value) sessionStorage.setItem('sessionApp', data2.value);}</script>子组件-
<template> <div> 子组件 </div> <button @click="getData">获取local到数据</button> <h2>{{ data1 }}</h2> <h2>{{ data2 }}</h2></template>
<script setup>import { ref } from 'vue';const data1=ref('')const data2=ref('')const getData = ()=>{ data1.value=localStorage.getItem('localApp') data2.value=sessionStorage.getItem('sessionApp')}</script>这里我们同时使用localStorage.setItem和sessionStorage.setItem来将值上传到浏览器存储中,再让其他组件通过localStorage.getItem和sessionStorage.getItem获取到浏览器存储中的值,从而实现组件间的通信。
window对象全局挂载
将数据挂载在全局对象身上这个操作不是在vue中常见,但是也是可以实现组件通信的,因为原生JavaScript中就有能够访问window对象的功能,因此我们在vue中当然也是可以访问到window对象的,并且由于window是全局对象,挂载在它身上的实例是可以被所有组件访问。
但是这种方法是绝不被vue推荐的,因为他不会在遵循单向数据流原则,并且对于组件暴露出来给某个组件的数据,是丝毫没有私密性的可言,全部的组件都能够访问到该数据。
子组件-
<template> <div> 子组件数据-{{ data }} </div></template>
<script setup>import { ref } from 'vue';const data=ref('子组件的数据是100')window.ChildData=data;</script>父组件-
<template> <div> 父组件: </div> <button @click="updata">获取子组件的数据</button> <h1>{{ data }}</h1> <Child /></template>
<script setup>import Child from './components/demo4/child.vue';import { ref } from 'vue';const data=ref('')const updata=()=>{ data.value=window.ChildData}</script>通过将数据和方法挂载到 window 对象上,虽然可以轻松地在整个 Vue.js 应用中访问它们。但是单项数据流原则和组件数据的私密性更应该是我们在开发中应该关注的重点。 而且window上的属性全局变量容易引起命名冲突,尤其是当项目中引入了多个第三方库时,这些库也可能使用相同的变量名。随着项目规模的增长,维护全局挂载的数据会变得越来越困难,因为它缺乏结构化管理。