目录
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() {
// 使用爱情话语的store
const 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>