目录
pinia
安装 Pinia
存储+读取数据
修改数据(三种方式)
storeToRefs
getters
$subscribe
store组合式写法
组件通信
props
自定义事件
mitt
v-model
$attrs
$refs、$parent
provide、inject
slot
pinia
Pinia 是一个用于 Vue.js 的状态管理库,作为 Vuex 的替代方案,旨在提供更简单、更灵活的状态管理功能。它与 Vue 3 紧密集成,充分利用了 Vue 的 Composition API,提供了更直观的 API 和更强大的功能。
安装 Pinia
npm install pinia
操作 src/main.ts
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';const pinia = createPinia();
const app = createApp(App);app.use(pinia);
app.mount('#app');
存储+读取数据
Store 是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。
在 Pinia 中,可以使用 defineStore 来定义一个 store。这个 store 包括 state、getters 和 actions,相当于组件中的: `data`、 `computed` 和 `methods`。
import { defineStore } from 'pinia';
import axios from 'axios';
import { nanoid } from 'nanoid';// 定义一个名为useLoveTalkStore的Pinia存储模块
export const useLoveTalkStore = defineStore('loveTalk', {// 定义模块的状态state: () => ({talkList: [{ id: 'yuysada01', content: '你今天有点怪,哪里怪?怪好看的!' },{ id: 'yuysada02', content: '草莓、蓝莓、蔓越莓,你想我了没?' },{ id: 'yuysada03', content: '心里给你留了一块地,我的死心塌地' }]}),// 定义模块的动作actions: {// 异步获取情话并添加到列表中async getLoveTalk() {try {// 发起HTTP GET请求获取随机情话const { data: { content: title } } = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json');// 生成唯一ID并创建新的情话语对象const obj = { id: nanoid(), content: title };// 将新的情话添加到列表后面this.talkList.push(obj);} catch (error) {// 请求失败时输出错误信息console.error('Error fetching love talk:', error);}}}
});
<template><div><!-- 按钮用于获取爱情话语 --><button @click="fetchLoveTalk">Get Love Talk</button><!-- 循环显示爱情话语列表 --><ul><li v-for="talk in loveTalkStore.talkList" :key="talk.id">{{ talk.content }}</li></ul></div>
</template><script lang="ts">
import { defineComponent } from 'vue';
import { useLoveTalkStore } from '@/store/talk';export default defineComponent({setup() {// 使用爱情话语的storeconst loveTalkStore = useLoveTalkStore();/*** 获取爱情话语的方法*/const fetchLoveTalk = async () => {await loveTalkStore.getLoveTalk();};return { loveTalkStore, fetchLoveTalk };}
});
</script>
修改数据(三种方式)
直接修改
直接通过 store 的 state 属性来修改数据:
countStore.sum = 666;
这种方式简单直接,但通常建议避免直接修改,因为这可能绕过一些管理逻辑(比如响应式更新)。
批量修改
使用 $patch 方法批量更新多个属性:
countStore.$patch({sum: 999,school: 'atguigu'
});
$patch 方法允许一次性更新 store 的多个属性,并且它会保留之前的其他属性值。
通过 Action 修改
在 store 中定义 actions,以便在更新数据时可以包含额外的业务逻辑:
import { defineStore } from 'pinia';export const useCountStore = defineStore('count', {state: () => ({sum: 0,school: ''}),actions: {// 加increment(value: number) {if (this.sum < 10) {this.sum += value;}},// 减decrement(value: number) {if (this.sum > 1) {this.sum -= value;}}}
});
使用 actions 的好处在于可以在状态更新之前或之后执行额外的逻辑,比如检查条件、调用其他函数等。
组件中调用 Action
在组件中,可以通过 store 实例调用 actions:
import { useCountStore } from './stores/countStore'; // 假设 store 定义在 countStore 文件中export default {setup() {const countStore = useCountStore();// 示例:调用 increment 方法const incrementValue = 5;countStore.increment(incrementValue);// 示例:调用 decrement 方法const decrementValue = 2;countStore.decrement(decrementValue);}
};
storeToRefs
storeToRefs 函数将 Pinia store 中的 state 数据转换为 Vue 的 ref 对象,从而使得这些数据在模板中保持响应性。
这与 Vue 的 toRefs 函数类似,但 storeToRefs 特别针对 Pinia 的 store 设计,确保了 store 数据的响应式管理(`pinia`提供的`storeToRefs`只会将数据做转换,而`Vue`的`toRefs`会转换`store`中数据。)。使用 storeToRefs 使得组件模板中对 store 的引用更加清晰和简洁。
<template><div class="count"><h2>当前求和为:{{sum}}</h2></div>
</template><script setup lang="ts" name="Count">import { useCountStore } from '@/store/count'/* 引入storeToRefs */import { storeToRefs } from 'pinia'/* 得到countStore */const countStore = useCountStore()/* 使用storeToRefs转换countStore,随后解构 */const {sum} = storeToRefs(countStore)
</script>
storeToRefs 的优势
- 保持响应性: 使用 storeToRefs 可以确保 store 的属性在模板中保持响应性。它将 state 属性转换为 ref 对象,从而可以在模板中直接使用。
- 简化代码: 在模板中直接使用 storeToRefs 转化的属性,避免了对 countStore.sum 的直接引用,代码更简洁。
- 提高可维护性: 对于较大的 store,使用 storeToRefs 可以避免直接解构 state 对象,提高代码的可维护性和清晰度。
getters
getters 是用于从 state 中派生计算值的属性,它们可以看作是 state 的“计算属性”。
- 计算属性:getters 可以对 state 中的数据进行处理,返回一个计算后的值。
- 缓存:getters 是基于其依赖的 state 缓存的,只有当依赖的 state 发生变化时,getters 才会重新计算。
- 使用方法:可以在组件中直接访问 getters,它们就像普通的属性一样。
import { defineStore } from 'pinia';/*** 定义一个名为Counter的Pinia存储* 该存储用于管理计数器相关的状态和操作*/
export const useCounterStore = defineStore('counter', {/*** 定义存储的状态* 返回一个包含初始状态的对象*/state: () => ({count: 0, // 计数器的当前值message: '你好,Pinia!' // 欢迎消息}),/*** 定义Getter,用于处理状态并返回结果*/getters: {/*** 获取计数器值的两倍* @param state 当前的状态* @returns 计数器值的两倍*/doubledCount: (state) => state.count * 2,/*** 将消息转换为大写* @param state 当前的状态* @returns 大写的消息*/uppercaseMessage: (state) => state.message.toUpperCase()},/*** 定义可以修改状态的方法*/actions: {/*** 增加计数器的值*/increment() {this.count++;},/*** 更新消息* @param newMessage 新的消息内容*/updateMessage(newMessage) {this.message = newMessage;}}
});
<template><div><!-- 显示计算后的计数值 --><p>计数: {{ doubledCount }}</p><!-- 显示转换为大写的消息 --><p>消息: {{ uppercaseMessage }}</p><!-- 触发计数增加的操作按钮 --><button @click="increment">增加</button><!-- 输入框,用于用户输入新的消息 --><input v-model="newMessage" placeholder="输入新消息" /><!-- 触发消息更新的操作按钮 --><button @click="updateMessage">修改消息</button></div>
</template><script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/store/counterStore';// 获取计数和消息的计算属性以及定义新的消息引用
const counterStore = useCounterStore();
// 将store中的属性转换为Vue中的ref,以便在模板中使用
const { doubledCount, uppercaseMessage } = storeToRefs(counterStore);
const newMessage = ref('');// 增加计数
function increment() {counterStore.increment();
}// 更新消息
function updateMessage() {counterStore.updateMessage(newMessage.value);
}
</script>
$subscribe
$subscribe 是 Pinia 中用于侦听 state 变化的一个方法。可以用它来监控 state 的任何变化,并在变化发生时执行特定的操作。它允许对 store 的 state 进行观察和响应。
talkStore.$subscribe((mutate,state)=>{console.log('LoveTalk',mutate,state)localStorage.setItem('talk',JSON.stringify(talkList.value))
})
store组合式写法
import {defineStore} from 'pinia';export const useCounterStore = defineStore('counter', {state: () => ({count: 0,}),actions: {increment() {this.count++;},decrement() {this.count--;},},
});
<template><div><p>计数: {{ counterStore.count }}</p><button @click="counterStore.increment">增加</button><button @click="counterStore.decrement">减少</button></div>
</template><script setup>
import {useCounterStore} from '@/store/counterStore';const counterStore = useCounterStore();
</script>
组件通信
在 Vue 3 中,组件通信与 Vue 2 相比有一些显著的变化:
事件总线:
- Vue 2:常用事件总线进行组件间的通信。
- Vue 3:推荐使用 mitt 或其他轻量级的事件库,作为事件总线的替代方案。mitt 是一个小巧的事件发射器,可以很方便地用来处理组件之间的事件。
状态管理:
- Vue 2:使用 vuex 进行状态管理。
- Vue 3:pinia 作为推荐的状态管理库,提供了更简洁的 API 和更好的 TypeScript 支持。
双向绑定:
- Vue 2:使用 .sync 修饰符来处理双向绑定。
- Vue 3:将 .sync 的功能优化到了 v-model 中,使得双向绑定更加一致和简洁。你可以通过在 v-model 上自定义事件名称来实现类似 .sync 的功能。
事件和属性:
- Vue 2:使用 $listeners 来访问和传递事件监听器。
- Vue 3:将 $listeners 和其他属性合并到 $attrs 中,可以通过 v-bind="$attrs" 来传递这些属性。
子组件访问:
- Vue 2:可以通过 $children 访问子组件。
- Vue 3:去掉了 $children,推荐使用 ref 和 expose 来访问和操作子组件。使用 ref 可以更精确地控制子组件实例的引用。
props
在 Vue 中,props 是一种常用的组件间通信方式,主要用于 父组件到子组件 的数据传递。
父组件:
<template><div class="parent"><!-- 按钮用于触发获取随机情话的事件 --><button @click="fetchQuote">获取随机情话</button><!-- 子组件用于展示获取到的随机情话 --><ChildComponent :message="randomQuote" /></div>
</template><script setup>
import {ref} from 'vue';
import axios from 'axios';
import ChildComponent from './demos.vue';// 定义一个响应式变量来存储随机情话
const randomQuote = ref('');/*** 异步函数,用于从API获取随机情话*/
const fetchQuote = async () => {try {// 使用axios库从指定API获取随机情话const {data: {content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json');// 将获取到的情话内容赋值给randomQuote变量randomQuote.value = content;} catch (error) {// 如果发生错误,输出错误信息到控制台console.error('Failed to fetch quote:', error);}
};
</script><style scoped>
.parent {background-color: #f0f8ff; /* 轻微蓝色背景,用于区分页面其他部分 */padding: 20px;border-radius: 8px;
}
</style>
子组件
<template><!-- 这个模板用来显示从父组件传递的消息 --><div class="child"><p>{{ message }}</p></div>
</template><script setup>
import {defineProps} from 'vue';// 定义组件接受的props,包括一个名为message的字符串类型属性
const props = defineProps({message: String
});
</script><style scoped>
/* 为.child类定义样式,使其具有轻微的绿色背景,适当的内边距和圆角 */
.child {background-color: #e6ffe6; /* 轻微绿色背景 */padding: 10px;border-radius: 4px;margin-top: 10px;
}
</style>
总结
- 父传子:通过 props 传递数据,属性值通常是静态数据或者动态计算的值。
- 子传父:通过自定义事件(例如使用 emit)将数据传递回父组件。子组件触发事件,父组件处理事件并获取数据。
这种方式保证了数据流的单向性,父组件将数据传递给子组件,子组件通过事件向父组件反馈变化。
自定义事件
自定义事件是 Vue 中用来实现子组件与父组件之间通信的一种方式。
概述
自定义事件用于子组件向父组件传递信息。子组件通过 emit 方法触发事件,父组件通过 v-on 监听这些事件并响应。
区分原生事件与自定义事件
原生事件:
- 事件名是固定的,如 click、mouseenter 等。
- 事件对象 $event 包含关于事件的详细信息,如 pageX、pageY、target 和 keyCode。
自定义事件:
- 事件名是任意名称,如 update, customEvent 等。
- 事件对象 $event 是触发事件时通过 emit 方法传递的数据,可以是任何类型的值。
子组件
<template><div class="parents"><button @click="fetchQuote">获取随机情话</button></div>
</template><script setup>
import {defineEmits} from 'vue';
import axios from 'axios';// 定义可用于子组件向父组件发送信息的自定义事件
const emit = defineEmits(['quoteReceived']);/*** 异步获取随机情话并发送到父组件*/
const fetchQuote = async () => {try {// 使用axios库从API获取随机情话数据const {data: {content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json');// 触发自定义事件,将获取到的情话内容传递给父组件emit('quoteReceived', content);} catch (error) {// 在控制台记录获取情话失败的错误信息console.error('获取情话失败:', error);}
};
</script><style scoped>
.parents {background-color: #ffcccb; /* 浅红色背景 */border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;
}
</style>
父组件:
<template><div class="parent"><!-- 引入子组件,并监听quoteReceived事件 --><ChildComponent @quoteReceived="handleQuoteReceived" /><!-- 条件性渲染接收到的报价 --><p v-if="quote">{{ quote }}</p></div>
</template><script setup>
import {ref} from 'vue';
import ChildComponent from './ChildComponent.vue';// 定义一个响应式变量quote来存储报价信息
const quote = ref('');/*** 处理接收到的报价数据* @param {string} receivedQuote - 接收到的报价字符串*/
const handleQuoteReceived = (receivedQuote) => {quote.value = receivedQuote;
};
</script><style scoped>
.parent {background-color: #e6e6ff; /* 浅蓝色背景 */padding: 20px;border-radius: 8px;
}
</style>
mitt
mitt 是一个轻量级的事件发布/订阅库,适用于组件间通信,特别是在 Vue.js 或其他前端框架中。它可以用来在不同组件间传递消息,而无需组件之间直接依赖。
安装 mitt
npm install mitt
创建一个事件总线
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
在子组件中发布事件
<template><button @click="sendMessage">发送消息</button>
</template><script setup>
import emitter from './eventBus.ts';const sendMessage = () => {emitter.emit('message', 'Hello from child!');
};
</script><style scoped>
button {background-color: #ffcccb;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;
}
</style>
在父组件中订阅事件
<template><div><ChildComponent /><p v-if="message">{{ message }}</p></div>
</template><script setup>
import {ref, onMounted, onUnmounted} from 'vue';
import emitter from './eventBus.ts';
import ChildComponent from './ChildComponent.vue';const message = ref('');const handleMessage = (msg) => {message.value = msg;
};onMounted(() => {emitter.on('message', handleMessage);
});onUnmounted(() => {emitter.off('message', handleMessage);
});
</script><style scoped>
div {background-color: #e6e6ff;padding: 20px;border-radius: 8px;
}
</style>
v-model
v-model 是 Vue.js 中用于双向数据绑定的指令,它可以轻松地实现父子组件之间的通信。在 Vue.js 中,v-model 通常用于表单元素(如 <input>, <textarea>, <select>),但它也可以用于自定义组件以实现父子组件间的双向数据绑定。
基础示例
假设我们有一个父组件和一个子组件,父组件希望通过 v-model 来双向绑定子组件中的数据。
子组件
<template><div><input v-bind="inputProps" @input="updateValue"/></div>
</template><script setup>
import {defineProps, defineEmits} from 'vue';const props = defineProps({modelValue: String, // 这里定义了 v-model 的绑定值
});const emit = defineEmits(['update:modelValue']); // 触发事件来更新 v-model 绑定的值const updateValue = (event) => {emit('update:modelValue', event.target.value); // 当输入值改变时,触发事件更新父组件的数据
};
</script>
<style scoped>
div {background-color: #ffcccb;padding: 20px;border-radius: 8px;
}
</style>
父组件
<template><div><ChildComponent v-model="parentValue" /><p>父组件中的值: {{ parentValue }}</p></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const parentValue = ref(''); // 父组件中的数据</script>
<style scoped>
div {background-color: #e6e6ff;padding: 20px;border-radius: 8px;
}
</style>
$attrs
概述
$attrs 是 Vue.js 提供的一个特殊对象,用于在当前组件中接收并传递父组件传递的属性,这些属性未被当前组件声明为 props。它支持从祖组件到孙组件的通信。
具体说明
$attrs 包含所有父组件传递的非 props 属性(如 HTML 属性、用户自定义属性等),但排除了已在子组件中声明的 props。这样,子组件可以通过 $attrs 将这些属性继续传递给孙组件,确保属性链的完整传递。
父组件:
<template><div class="father"><h3>父组件</h3><Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from "vue";let a = ref(1)let b = ref(2)let c = ref(3)let d = ref(4)function updateA(value){a.value = value}
</script>
子组件:
<template><div class="child"><h3>子组件</h3><GrandChild v-bind="$attrs"/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template><div class="grand-child"><h3>孙组件</h3><h4>a:{{ a }}</h4><h4>b:{{ b }}</h4><h4>c:{{ c }}</h4><h4>d:{{ d }}</h4><h4>x:{{ x }}</h4><h4>y:{{ y }}</h4><button @click="updateA(666)">点我更新A</button></div>
</template><script setup lang="ts" name="GrandChild">defineProps(['a','b','c','d','x','y','updateA'])
</script>
$refs、$parent
$refs
用途:父组件 → 子组件
概述:$refs 是一个对象,包含所有被 ref 属性标识的 DOM 元素或子组件实例。它允许你直接访问子组件或 DOM 元素,通常用于在父组件中调用子组件的方法或操作 DOM。
$parent
用途:子组件 → 父组件
概述:$parent 是当前组件实例的父组件实例对象。通过 $parent,子组件可以访问父组件的方法、数据或其他属性。然而,过度使用 $parent 可能会使组件之间的耦合变得紧密,影响可维护性。
示例
<template><div><h1>{{ message }}</h1><ChildComponent ref="childComponentRef" /><button @click="callChildMethod">调用子组件的方法</button></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';// 定义数据
const message = ref('这是父组件的消息');// 定义方法
const childComponentRef = ref(null);const callChildMethod = () => {childComponentRef.value.changeMessageInParent();
};const updateMessage = (newMessage) => {message.value = newMessage;
};// 暴露方法给子组件
defineExpose({updateMessage
});
</script>
<template><div><button @click="updateParentMessage">更新父组件的消息</button></div>
</template><script setup>
import {getCurrentInstance} from 'vue';// 获取父组件实例
const parent = getCurrentInstance().parent;// 定义方法
const updateParentMessage = () => {parent.exposed.updateMessage('子组件更新了父组件的消息');
};const changeMessageInParent = () => {parent.exposed.updateMessage('子组件方法被调用了');
};// 暴露方法给父组件
defineExpose({changeMessageInParent
});
</script>
provide、inject
provide 和 inject 是 Vue 3 中用于实现祖孙组件直接通信的机制。它们允许祖先组件向其所有的后代组件传递数据或方法,而不需要逐层传递 props。这在复杂的组件树中尤其有用,可以避免 prop drilling(逐层传递 props)。
概述
- provide:在祖先组件中定义并提供数据或方法,供后代组件使用。
- inject:在后代组件中声明并接收祖先组件提供的数据或方法。
示例
父组件
<template><div style="background-color: #e0f7fa; padding: 20px;"><h1>{{ message }}</h1><ChildComponent /></div>
</template><script setup>
import {provide, ref} from 'vue';
import ChildComponent from './ChildComponent.vue';// 定义数据
const message = ref('这是父组件的消息');// 提供数据和方法
provide('message', message);
provide('updateMessage', (newMessage) => {message.value = newMessage;
});
</script>
子组件
<template><div style="background-color: #b2ebf2; padding: 20px;"><button @click="updateParentMessage">更新父组件的消息</button><GrandChildComponent /></div>
</template><script setup>
import {inject} from 'vue';
import GrandChildComponent from './GrandChildComponent.vue';// 注入数据和方法
const message = inject('message');
const updateMessage = inject('updateMessage');// 定义方法
const updateParentMessage = () => {updateMessage('子组件更新了父组件的消息');
};
</script>
孙子组件
<template><div style="background-color: #80deea; padding: 20px;"><button @click="updateParentMessage">孙子组件更新父组件的消息</button></div>
</template><script setup>
import { inject } from 'vue';// 注入数据和方法
const message = inject('message');
const updateMessage = inject('updateMessage');// 定义方法
const updateParentMessage = () => {updateMessage('孙子组件更新了父组件的消息');
};
</script>
slot
在 Vue 3 中,slot 是一种强大的内容分发机制,允许父组件向子组件传递内容。slot 可以分为三种类型:默认插槽、具名插槽和作用域插槽。
- 默认插槽:默认插槽是最简单的插槽类型,父组件传递的内容会被插入到子组件的默认插槽中。
- 具名插槽:具名插槽允许父组件向子组件的不同位置传递内容,通过 name 属性来区分不同的插槽。
- 作用域插槽:作用域插槽允许子组件向父组件传递数据,父组件可以通过作用域插槽访问这些数据。
父组件
<template><div style="background-color: #e0f7fa; padding: 20px;"><h1>{{ message }}</h1><ChildComponent><!-- 具名插槽 --><template #header><h2>这是父组件传递的头部内容</h2></template><!-- 作用域插槽 --><template #default="slotProps"><p>这是父组件传递的默认内容</p><p>{{ slotProps.text }}</p></template><template #footer><p>这是父组件传递的尾部内容</p></template></ChildComponent></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';// 定义数据
const message = ref('这是父组件的消息');
</script>
子组件
<template><div style="background-color: #b2ebf2; padding: 20px;"><header><slot name="header"></slot></header><main><slot :text="slotData"></slot></main><footer><slot name="footer"></slot></footer></div>
</template><script setup>
import { ref } from 'vue';// 定义数据
const slotData = ref('这是子组件传递的数据');
</script>