【Vue3】(三)vue3中的pinia状态管理、组件通信

目录

一、vue3的pinia

二、【props】传参

三、【自定义事件】传参

四、【mitt】传参

五、【v-model】传参(平常基本不写)

[六、【attrs】传参](#六、【attrs】传参)

七、【refs和parent】传参

八、provide和inject


一、vue3的pinia

1、什么是pinia?

pinia 是一个 Vue3 的状态管理库,它的 API 设计和 Vuex 有很大的相似之处,但是它的实现方式和 Vuex 完全不同,它是基于 Vue3 的新特性 Composition API 实现的,所以它的使用方式和 Vuex 也有很大的不同。

Pinia | The intuitive store for Vue.js值得你喜欢的 Vue Storehttps://pinia.vuejs.org/zh/

2、为什么Vue3选择pinia?

在构建大型或中型Vue应用时,组件之间的状态共享和管理是一个不可避免的挑战。Vue.js的官方状态管理库Vuex在过去几年里一直是解决这个问题的主流方案。但是,Vuex的复杂性和对TypeScript支持的限制促使社区寻找更简洁、更灵活的解决方案。这就是Pinia应运而生的背景。

Pinia是Vue.js的一个全新状态管理库库,由同一个团队编写,旨在提供一个更轻量级和用户友好的状态管理体验。它以简单直观的API、完全的TypeScript支持和更好的开发体验而受到欢迎。

3、使用pinia的好处

简化的API、更好的TypeScript集成、开箱即用的DevTools集成、模块化和灵活性、易于测试、轻量级、持久化和插件支持

Pinia提供了一个更加直观和简洁的API,使得状态管理变得更加容易理解和实施。对于开发者来说,这意味着更少的学习曲线和更快的开发速度。由于Pinia自底向上设计了对TypeScript的支持,使用TypeScript的开发者将会享受到无缝的类型推导和更少的类型断言。Pinia与Vue DevTools的集成提供了更好的调试体验,允许开发者轻松追踪和操作状态,从而提高开发效率。Pinia支持将状态分割为不同的模块,使得状态管理在大型应用中更加清晰和可维护 。此外,它的灵活性允许你根据需要轻松地添加插件和中间件。由于Pinia的设计,编写单元测试变得更加直观。你可以轻松地模拟actions和测试state的变更。Pinia的代码库比Vuex更小,对于注重应用大小的项目来说,这是一个明显的优势。Pinia支持通过插件来扩展其功能,例如状态持久化,这使得在浏览器刷新或关闭后恢复用户状态变得简单。

综上所述,++Pinia为开发Vue应用的状态管理提供了一个现代化、高效和灵活的解决方案。随着Vue 3的推进,Pinia正在成为越来越多Vue开发者的首选状态管理库++。

4、安装pinia

1、安装依赖

**安装pinia依赖:**npm i pinia

2、项目配置

目录:main.ts

javascript 复制代码
// 引入pinia
import { createPinia } from "pinia";
// 创建pinia
const pinia = createPinia()
// 安装pinia
app.use(pinia)

3、存储/读取pinia中的数据

(1)在src下创建store目录

(2)在store目录下创建ts文件(最好能明确地看出属于哪个组件使用)

例如: count.ts

javascript 复制代码
import { defineStore } from "pinia";
// 官方推荐使用Hooks的方式命名
export const useCountStore = defineStore('count',{
  // 状态/数据(官方要求必须写成一个函数)
  // state: 真正存储数据的地方(类似于Vue2中的data)
  state(){
    return{
      sum: 6
    }
  }
})

(3)读取pinia中的值

javascript 复制代码
// 类似Hooks的思想
import { useCountStore } from "@/store/count";

const countStore = useCountStore()

// 以下两种方式都可以拿到state中的数据
console.log('countStore', countStore.sum)
console.log('countStore', countStore.$state.sum)

4、修改pinia中的数据

(1)修改单个数据:直接修改

javascript 复制代码
 countStore.sum += 1

(2)修改多个数据:$patch 批量修改

javascript 复制代码
countStore.$patch({
  sum:888,
  address:'陕西',
  city:'西安'
})

**(1)和(2)的区别:**方法(1)在修改多个数据的时候,会触发多个mutatuons事件,但是方法(2)在修改数据的时候,只会触发一个$patch事件

(3)使用action修改数据:处理复杂数据时使用

  • 需要在pinia中定义action函数
javascript 复制代码
export const useCountStore = defineStore('count',{
  // actions里面放置的是一个一个的方法,用于响应组件中的动作
  actions:{
    increment(value){
      console.log('increment被调用了',value)
      // 修改数据
      // this是当前的store
      console.log('this.sum', this.sum)
      this.sum += value
    }
  },
  // 状态/数据(官方要求必须写成一个函数)
  // state: 真正存储数据的地方(类似于Vue2中的data)
  state(){
    return{
      sum: 6,
      city:'山西',
      address: '未知'
    }
  }
})
  • 使用action中定义的修改数据的函数
javascript 复制代码
 // 第三种修改数据的方式:使用action属性修改数据
    countStore.increment(n.value)

5、storeToRefs(保持store中数据的响应式)

  • toRefs会将所包裹的数据全部变成ref对象,包括数据和方法,在这里不适用(vue3自带的)
  • storeToRefs只会关注store中的数据,不会对方法进行ref包裹(pinia自带的)
javascript 复制代码
import { storeToRefs } from "pinia";
// 解构赋值countStore中的数据
// 1、为了保证数据的响应式,需要使用storeToRefs
// 2、storeToRefs只会关注store中的数据,不会对方法进行ref包裹
const {sum,city,address} = storeToRefs(countStore)

6、getters

**概念:**当state中的数据,需要经过处理后再使用,可以使用getters配置。

在store中追加getters配置

javascript 复制代码
export const useCountStore = defineStore('count',{
  // state: 真正存储数据的地方(类似于Vue2中的data)
  state(){
    return{
      sum: 6,
      city:'山西',
      address: 'chinese'
    }
  },
  // 配置getters
  getters:{
    // 写法一:
    bigSum:(state)=>{
      return state.sum * 10
    },
    // 写法二:
    upperaddress(){
      // this可以拿到当前这个store对象
      console.log('this', this)
      return this.address.toUpperCase()
    }
  }
})

使用getters处理后的数据

html 复制代码
<template>
  <div class="count">
    <h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2>
    <h2>当前地址为:{{ address }},转换成大写后:{{ upperaddress }}</h2>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
// Hooks的思想
import { useCountStore } from "@/store/count";
import { storeToRefs } from "pinia";

const countStore = useCountStore();

// 解构赋值countStore中的数据
// 1、为了保证数据的响应式,需要使用storeToRefs
// 2、storeToRefs只会关注store中的数据,不会对方法进行ref包裹
const { sum, city, address, bigSum, upperaddress } = storeToRefs(countStore);

</script>

7、监听store中数据的变化:$subscribe

  • 订阅方法的使用:类似watch属性,可以监听state及其变化
  • 两个参数:本次修改的信息真正的数据
javascript 复制代码
talkStore.$subscribe((mutate, state) => {
  console.log("$subscribe监听事件的变化", mutate, state);
  // 实现一个无感刷新数据的方法
  localStorage.setItem('talkList',JSON.stringify(state.talkList))  // 将数据存到本地存储
});

8、store的组合式写法

javascript 复制代码
export const useTalkStore = defineStore('talk', () => {
  // 直接定义的数据,相当于state
  const talkList = reactive(
    JSON.parse(localStorage.getItem('talkList') as string) || []
  )
  // 直接定义的方法,就相当于action
  async function getATalk() {
    // 发请求
    // {data:{content:title}} => 这个写法的意思是:将返回的结果对象赋值给data,然后再结构data给content,最后再把content赋值给title
    let {
      data: { content: title },
    } = await axios.get("https://api.uomg.com/api/rand.qinghua?fromat=json");
    // 将请求回来的字符串包装成一个对象
    let obj = { id: nanoid(), title };
    console.log("obj", obj);
    talkList.unshift(obj);
  }

  // 注意:最后一定要return,要不然没法使用
  return {talkList,getATalk}
})

二、【props】传参

概述: props是使用频率最高的一种通信方式。**作用:**父传子、子传父

  • 父传子 :属性值是非函数(变量)
  • 子传父 :属性值是函数(方法)

父组件:

html 复制代码
<template>
  <div class="father">
    <h3>父组件1</h3>
    <h4>父亲的车:{{ car }}</h4>
    <!-- 父传子【参数】:使用 属性传参 的方式传递参数 -->
    <!-- 父传子【方法】:使用 属性传参 的方式传递参数-->
    <h5>父组件接收到子组件的参数:{{ sonParams }}</h5>
    <Child :dirver="car" :sendSon="getSonParams"/>
  </div>
</template>
<script setup lang="ts" name="father">
import Child from "./Child.vue";
import { ref } from "vue";

let car = ref('小米')
let sonParams = ref()

// 方法
function getSonParams(params:type) {
  console.log('params', params)
  sonParams.value = params
}
</script>

子组件:

html 复制代码
<template>
  <div class="child"> 
    <h3>子组件2</h3>
    <h4>儿子的玩具:{{ toy }}</h4>
    <!-- 子组件使用父组件参数 -->
    <h5>子组件拿到父组件的参数:{{ dirver }}</h5>
    <!-- 复杂写法 -->
    <button @click="handleFatherFun">方法1:触发父组件的方法实例</button>
    <br />
    <button @click="sendSon(toy)">方法2:把子组件的参数传递给父组件</button>
  </div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";

let toy = ref('奥特曼') 

// 子组件接收:声名接收函数:defineProps 数组
let props = defineProps(['dirver','sendSon'])

function handleFatherFun(params:type) {
  // 使用props.拿到父组件传递的参数
  props.sendSon(toy.value)
}
</script>

图解代码:

三、【自定义事件】传参

在 Vue3 中,自定义事件是一种常用的机制,用于在组件之间传递数据。通过自定义事件,子组件可以向父组件发送数据,从而实现组件间的通信。

父组件:

html 复制代码
<template>
  <div>
    <h3>父组件</h3>
    <!-- 给子组件Child绑定事件 -->
    <h4 v-show="toy">子给的玩具:{{ toy }}</h4>
    <!-- 官方推荐:自定义事件使用kabab-case的事件名(a-b) -->
    <Child @send-toy="saveToy"/>
  </div>
</template>

<script setup lang="ts">
import Child from "./child.vue";
import { ref } from "vue";

let toy = ref('')
// 用于处理子组件传递过来的数据
function saveToy(value:any) {
  console.log('saveToy', value)
  toy.value = value
}
</script>

子组件:

html 复制代码
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <button @click="emit('send-toy',toy)"></button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

let toy = ref('奥特曼')
// 声明事件
let emit = defineEmits(['send-toy'])
</script>

图解代码:

四、【mitt】传参

1、认识mitt

mitt是vue3提供的一个工具,能够实现跨组件通信

**下载mitt依赖:**npm i mitt

① 创建mitt文件目录

目录地址:src/utils/emitter.ts

② 创建mitt对象

src/utils/emitter.ts

javascript 复制代码
// 引入itt
import mitt from 'mitt'
// 调用mitt,得到emitter,emitter能:绑定事件,触发事件
const emitter = mitt()
// 暴露emitter
export default emitter

③ 项目引入mitt(例如:main.ts)

javascript 复制代码
// 引入mitt
import emitter from "@/utils/emitter";

④ mitt语法介绍

javascript 复制代码
// 绑定事件
emitter.on('tast1',()=>{
  console.log('tast1被触发了')
})

emitter.on('tast2',()=>{
  console.log('tast2被触发了')
})

// 触发事件
setTimeout(() => {
  emitter.emit('tast1')
  emitter.emit('tast2')
}, 1000);

// 解除事件
setTimeout(() => {
  // 清除单个事件
  emitter.off('tast1')
  // 清除所有事件
  emitter.all.clear()
}, 3000);

2、使用mitt

组件1:

javascript 复制代码
<template>
  <div class="child1">
    <h2>子组件1</h2>
    <h3>玩具:{{ toy }}</h3>
    <button @click="emitter.emit('send-toy',toy)">玩具给Childe2玩</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import emitter from "@/utils/emitter";

let toy = ref('奥特曼')
</script>

组件2:

javascript 复制代码
<template>
  <div class="child2">
    <h2>子组件2</h2>
    <h3>电脑:{{ computer }}</h3>
    <h4>Child1传递的玩具{{ toy }}</h4>
  </div>
</template>

<script setup lang="ts">
import { ref,onUnmounted } from "vue";
import emitter from "@/utils/emitter";
// 数据
let computer = ref('联想')
let toy = ref()

// 给emitter绑定send-toy事件
emitter.on('send-toy',(value:any)=>{
  console.log('send-toy',value)
  toy.value = value
})
// 在组件卸载时,解绑emitter
onUnmounted(()=>{
  emitter.all.clear()
})
</script>

图解代码:

**注意:**当绑定事件的组件卸载时,最好清除掉emitter绑定的事件,防止内存占用

五、【v-model】传参(平常基本不写)

v-model传参方式一般适用于UI组件库底层的传参使用,所以,这里不做具体介绍,只说明底层原理,平常用的时候还是使用v-model即可

父组件:

javascript 复制代码
<template>
  <div class="father">
    <h3>父组件</h3>
    <!-- v-model用在html标签上 -->
    <!-- <input type="text" v-model="userName"/> -->
    <!-- v-model的底层原理: -->
    <!-- <input type="text" :value="userName" @input="userName = (<HTMLInputElement>$event.target).value"> -->

    <!-- v-model用在组件标签上 -->
    <!-- <Zhang v-model="userName"/> -->
    <!-- v-model实现组件传参的基本原理: -->
    <Zhang 
      :modelValue="userName" 
      @update:modelValue="userName = $event"
    />

  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import Zhang from "./zhang.vue";
// 数据
let userName = ref('张三')
</script>

子组件:

javascript 复制代码
<template>
  <div>
    <h2>子组件</h2>
    <input 
      type="text" 
      :value="modelValue"
      @input="emits('update:modelValue',(<HTMLInputElement>$event.target).value)"
    />
  </div>
</template>

<script setup lang="ts">
defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
</script>

拓展:

修改v-model的事件名称

六、【$attrs】传参

1、概述:$attrs用于实现当前组件的父组件 ,向当前组件的子组件通信(祖→孙)

2、具体说明:$attrs是一个对象,包含所有父组件传入的标签属性

注意:$attrs会自动排除props中声明的属性(可以任务声明过的props被子组件自己"消费"了),也就是说:当父(祖-父-子)组件中以props的方式使用了某个属性,在子组件中就不能使用这个属性了

javascript 复制代码
// 如果在父组件中用defineProps接收了a参数,那么a参数就没法继续传递了
defineProps(['a'])

祖组件:

html 复制代码
<template>
  <div class="father">
    <h2>父组件</h2>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>
    <h4>c:{{ c }}</h4>
    <h4>d:{{ d }}</h4>
    <Child :a="a"
           :b="b"
           :c="c"
           :d="d"
           v-bind="{x:200,y:100}"
           :updateA="updateA" />
  </div>
</template>

<script setup lang="ts">
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>

父组件:

html 复制代码
<template>
  <div class="child">
    <h2>子组件</h2>
    <GrandChild v-bind="$attrs"/>
  </div>
</template>

<script setup lang="ts">
import GrandChild from "./grandChild.vue";
// 如果这里在defineProps接收了a参数,那么a参数就没法继续传递了
defineProps(['a'])
</script>

孙组件:

html 复制代码
<template>
  <div class="grand-child">
    <h2>孙组件</h2>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>
    <h4>c:{{ c }}</h4>
    <h4>d:{{ d }}</h4>
    <button @click="updateA(6)">点我将爷爷组件中的a更新</button>
  </div>
</template>

<script setup lang="ts">
defineProps(['a','b','c','d','x','y','updateA'])
</script>

七、【refs和parent】传参

1、概述

  • $refs:用于父→子
  • $parent:用于子→父

2、原理

属性 说明
$refs 值为对象,包含所有ref属性标识的DOM元素或组件实例
$parent 值为对象,当前组件的父组件实例对象

3、组件使用实例

父组件:

html 复制代码
<template>
  <div class="father">
    <h2>父组件</h2>
    <h4>房产:{{ house }}</h4>
    <button @click="changeToy">修改Child1的玩具</button>
    <button @click="changeComputer">修改Child2的电脑</button>
    <button @click="getAllChild($refs)">所有孩子的书增加</button>
    <Child1 ref="c1" />
    <Child2 ref="c2" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import Child1 from "./child1.vue";
import Child2 from "./child2.vue";

// 数据
let house = ref(4);
let c1 = ref();
let c2 = ref();

function changeToy() {
  console.log("c1.value", c1.value);
  c1.value.toy = "小猪佩奇";
}

function changeComputer() {
  console.log("c2.value", c2.value);
  c2.value.toy = "华为";
}

function getAllChild(refs: { [key:string]: any }) {
  console.log("所有子组件", refs);
  for (const key in refs) {
    console.log("refs", refs[key]);
    refs[key].book += 3;
  }
}

defineExpose({house})
</script>

子组件1:

html 复制代码
<template>
  <div class="child1">
    <h2>子组件1</h2>
    <h4>玩具:{{ toy }}</h4>
    <h4>书籍:{{ book }}本</h4>
    <button @click="minusHouse($parent)">干掉父组件的一套房产</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

// 数据
let toy = ref('奥特曼')
let book = ref(3)

function minusHouse(parent:any) {
  console.log('parent', parent)
  parent.house -= 1 
}

// 把数据交给外部
defineExpose({toy,book})
</script>

子组件2:

html 复制代码
<template>
  <div class="child2">
    <h2>子组件2</h2>
    <h4>电脑:{{ computer }}</h4>
    <h4>玩具:{{ book }}</h4>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

// 数据
let computer = ref('联想')

let book = ref('奥特曼')

// 把数据交给外部
defineExpose({computer,book})
</script>

4、refs和parent使用精髓

  • 父组件中,给子组件绑定ref属性,通过$refs可以获取到所有的子组件实例
  • 子组件 ,只有通过defineExpose宏函数,将想要暴露的数据暴露出去,父组件才能拿到数据

八、provide和inject

能够完全实现不借助于中间组件跨组件通信

父组件:

html 复制代码
<template>
  <div class="father">
    <h2>父组件</h2>
    <h3>银子:{{ money }}元</h3>
    <h3>车子:一辆{{car.brand}},价值:{{car.price}}万元</h3>
    <Child />
  </div>
</template>

<script setup lang="ts">
import Child from "./child.vue";
import { ref, reactive, provide } from "vue";

let money = ref(100);
let car = reactive({
  brand: "奔驰",
  price: 1000,
});
// 孙组件修改祖先组件的money数据
function updateMoney(value: number) {
  money.value -= value;
}

// provide传参:向后代提供数据
provide("moneyContext", { money, updateMoney });
provide("car", car);
</script>

子组件:

html 复制代码
<template>
  <div class="grandChild">
    <h2>孙组件</h2>
    <h3>父组件【银子】:{{ fatherMoney }}</h3>
    <h3>父组件【车子】:一辆{{fatherCar.brand}},价值:{{fatherCar.price}}万元</h3>
    <button @click="updateMoney(6)">修改money</button>
  </div>
</template>

<script setup lang="ts">
import { inject } from "vue";

let { fatherMoney, updateMoney } = inject("moneyContext", {
  fatherMoney: 0,
  updateMoney: (value:number) => {},
});
let fatherCar = inject("car", { brand: "未知", price: "未知" });
</script>
相关推荐
ohMyGod_1231 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js
前端小趴菜051 小时前
React-forwardRef-useImperativeHandle
前端·vue.js·react.js
@大迁世界1 小时前
第1章 React组件开发基础
前端·javascript·react.js·前端框架·ecmascript
Hilaku1 小时前
从一个实战项目,看懂 `new DataTransfer()` 的三大妙用
前端·javascript·jquery
爱分享的程序员1 小时前
前端面试专栏-算法篇:20. 贪心算法与动态规划入门
前端·javascript·node.js
我想说一句1 小时前
事件委托与合成事件:前端性能优化的"偷懒"艺术
前端·javascript
爱泡脚的鸡腿1 小时前
Web第二次笔记
前端·javascript
良辰未晚1 小时前
Canvas 绘制模糊?那是你没搞懂 DPR!
前端·canvas
Dream耀1 小时前
React合成事件揭秘:高效事件处理的幕后机制
前端·javascript
P7Dreamer1 小时前
Vue 3 + Element Plus 实现可定制的动态表格列配置组件
前端·vue.js