Vue3学习(三)Pinia、组件通信

365 阅读8分钟

Pinia

Pinia是vue的状态管理库。像一些想要多个组件公用的数据,可以交给pinia管理

pinia存储数据

pinia存取数据,要放在store目录下的各个ts文件中。例:

import { defineStore } from "pinia";
export const useCountStore = defineStore('Count',{
    state(){
        return {
            sum:6
        }
    }
})
import { defineStore } from "pinia";
export const useTalkStore = defineStore('Talk',{
    state(){
        return {
            talkList:[
                {id:'001',title:'羊粑粑'},
                {id:'002',title:'oi'},
                {id:'003',title:'乌拉'}
            ]
        }
    }
})

将要存储的数据放在state配置项中,此配置项要写做一个函数,然后return所有要由pinia存储的数据。

导出的那个部分一般规范写作useXxxStore。

获取pinia存储的数据

例:

<script lang="ts" setup>
    import { ref } from 'vue';
    import {useCountStore} from '@/store/Count';
    const countStore = useCountStore()
    let n = ref(1)

    function add(){
        countStore.sum+=n.value
    }
    function dec(){
        countStore.sum-=n.value
    }
</script>

要先引入store下的ts文件默认导出的那个useXxxStore,比如此处是useCountStore,用变量接收后,此变量的名字一般是将前面的use去掉。

要获取pinia存储的数据,直接 xxxStore.数据名 获取即可。获取出来的数据是响应式的。

修改pinia存储的数据

修改pinia存储的数据有三种方式:

  • 第一种方式就像上面的案例中的add和dec方法一样,直接xxxStore.数据名 = ……

  • 第二种方式是使用patch的方式,可以同时修改多个pinia存储的数据。

    countStore.$patch({
          sum:99,
          school:'井冈山大学',
          address:'吉安'
        })
    
  • 第三种方式是使用actions配置项的方式,actions配置项里面放置的是一个一个的方法,用于响应组件中的“动作”。

    import { defineStore } from "pinia";
    export const useCountStore = defineStore('Count',{
        actions:{
            imcrement(n:number){
                this.sum += number
            }
        },
        state(){
            return {
                sum:6
            }
        }
    })
    

    要调用state中存储的数据,直接this.数据名即可。

    使用:

    function add(){
            // countStore.sum+=n.value
            countStore.imcrement()
        }
    

如果觉得使用xxxStore.数据名比较不美观,可以将响应的数据解构出来,但是,解构的时候要让其数据是响应式的,要使用到一个函数:storeToRefs()

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }}</h2>
    <select v-model="n">
        <option :value="1">1</option>
        <option :value="2">2</option>
        <option :value="3">3</option>
    </select>
    <button @click="add"></button>
    <button @click="dec"></button>
  </div>
</template>

<script lang="ts" setup>
    import { ref } from 'vue';
    import {useCountStore} from '@/store/Count';
    import { storeToRefs } from 'pinia';
    const countStore = useCountStore()
    let {sum} = storeToRefs(countStore)
    let n = ref(1)

    function add(){
        countStore.imcrement(n.value)
    }
    function dec(){
        countStore.sum-=n.value
    }
</script>

如此,解构出来的sum就是一个响应式的值。

getters

要是想要对pinia中存储的数据进行加工,类似于计算属性,可以使用getters配置项:

import { defineStore } from "pinia";
export const useCountStore = defineStore('Count',{
    actions:{
        imcrement(n:number){
            this.sum += n
        }
    },
    state(){
        return {
            sum:6
        }
    },
    getters:{
        bigSum(state){
            return state.sum * 10
        }
        //也可以写作 :state => state.sum * 10
    }
})

如上,要取出这个bigSum数据,和state中的数据的获取方式是一样的,也是上面那三种方式。

$subscribe的使用

subscibe的作用类似于watch监视

通过 store 的 $subscribe() 方法侦听 state 及其变化

talkStore.$subscribe((mutate,state)=>{
  console.log('LoveTalk',mutate,state)
  localStorage.setItem('talk',JSON.stringify(talkList.value))
})

mutate是修改前的数据,state是修改后的数据。可以在其中做一些类似于监视的逻辑处理。

store组合式写法

将defineStore函数的第二个参数写作箭头函数的格式,在其中写组合式的形式的ts代码:

import { defineStore } from "pinia";
import axios from 'axios';
import {nanoid} from 'nanoid';
import { reactive } from "vue";
export const useTalkStore = defineStore('Talk',()=>{
    const talkList = reactive([
        {id:'001',title:'羊粑粑'},
        {id:'002',title:'oi'},
        {id:'003',title:'乌拉'}
    ]);

    async function getAllTalk(){
        let result = await axios.post('<https://tenapi.cn/v2/yiyan>')
        console.log(result)
        let {data} = result
        let obj = {id:nanoid(),title:data}
        talkList.unshift(obj)
        console.log(talkList)
    }

    return {talkList,getAllTalk}
})

组件通信

方式1:props

父组件可以利用props将数据传给子组件,子组件可以通过父组件props传过来的方法,将数据传给父组件。例:

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
		<h4>汽车:{{ car }}</h4>
		<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
		<Child :car="car" :sendToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import {ref} from 'vue'
	// 数据
	let car = ref('奔驰')
	let toy = ref('')
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子组件:

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>玩具:{{ toy }}</h4>
		<h4>父给的车:{{ car }}</h4>
		<button @click="sendToy(toy)">把玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import {ref} from 'vue'
	// 数据
	let toy = ref('奥特曼')
	// 声明接收props
	defineProps(['car','sendToy'])
</script>

方式二:自定义事件

父组件也可以定义自定义事件让子组件接收,让子组件去触发这个自定义事件,完成数据的传递。

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
		<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
		<!-- 给子组件Child绑定事件 -->
    <Child @send-toy="saveToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	let toy = ref('')
	// 用于保存传递过来的玩具
	function saveToy(value:string){
		console.log('saveToy',value)
		toy.value = value
	}
</script>

子组件:

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>玩具:{{ toy }}</h4>
		<button @click="emit('send-toy',toy)">把玩具给父组件</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	// 数据
	let toy = ref('奥特曼')
	// 声明事件
	const emit =  defineEmits(['send-toy'])
</script>

方式三:mitt

可以使用mitt,来绑定事件。

首先,先写好emitter.ts,使用mitt获取得到一个emitter对象暴露出去。

// 引入mitt
import mitt from 'mitt';

// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()

// 暴露emitter
export default emitter

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <Child1/>
    <Child2/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child1 from './Child1.vue'
  import Child2 from './Child2.vue'
</script>

子组件1:

<template>
  <div class="child1">
    <h3>子组件1</h3>
		<h4>玩具:{{ toy }}</h4>
		<button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button>
  </div>
</template>

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

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

子组件2:

<template>
  <div class="child2">
    <h3>子组件2</h3>
		<h4>电脑:{{ computer }}</h4>
		<h4>哥哥给的玩具:{{ toy }}</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
	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)=>{
		toy.value = value
	})
	// 在组件卸载时解绑send-toy事件
	onUnmounted(()=>{
		emitter.off('send-toy')
	})
</script>

如此便能实现兄弟组件的数据传递。

方式四:使用v-model

组件的v-model,底层其实还是使用自定义事件来实现的。例如:

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>{{ username }}</h4>
    <h4>{{ password }}</h4>
    <!-- v-model用在html标签上 -->
    <!-- <input type="text" v-model="username"> -->
    <!-- <input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"> -->

    <!-- v-model用在组件标签上 -->
    <!-- <AtguiguInput v-model="username"/> -->
    <!-- <AtguiguInput 
      :modelValue="username" 
      @update:modelValue="username = $event"
    /> -->

    <!-- 修改modelValue -->
    <AtguiguInput v-model:ming="username" v-model:mima="password"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import { ref } from "vue";
  import AtguiguInput from './AtguiguInput.vue'
  // 数据
  let username = ref('zhansgan')
  let password = ref('123456')
</script

子组件:即输入框的那个组件

<template>
  <input 
    type="text" 
    :value="ming"
    @input="emit('update:ming',(<HTMLInputElement>$event.target).value)"
  >
  <br>
  <input 
    type="text" 
    :value="mima"
    @input="emit('update:mima',(<HTMLInputElement>$event.target).value)"
  >
</template>

<script setup lang="ts" name="AtguiguInput">
  defineProps(['ming','mima'])
  const emit = defineEmits(['update:ming','update:mima'])
</script>

方式五:$attrs

如果父组件传给子组件props的数据,而子组件没有接收,那这些没有被接收的值就会变成$attrs的数据。例:

父组件:给子组件传递了六个props数据,有四个没被接收

<template>
  <div class="father">
    <h3>父组件</h3>
		<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: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:number){
		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(6)">点我将爷爷那的a更新</button>
	</div>
</template>

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

如此便可以实现多代通信。

方式六:refsrefs和parent

$refs用于:父传子

$parent用于:子传父

$refs值为对象,包含所有被ref属性标识的DOM元素活组件实例。

$parent值为对象,当前组件的父组件的实例对象。

例:

父组件:

<template>
	<div class="father">
		<h3>父组件</h3>
		<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" name="Father">
	import Child1 from './Child1.vue'
	import Child2 from './Child2.vue'
	import { ref,reactive } from "vue";
	let c1 = ref()
	let c2 = ref()

	// 注意点:当访问obj.c的时候,底层会自动读取value属性,因为c是在obj这个响应式对象中的
	/* let obj = reactive({
		a:1,
		b:2,
		c:ref(3)
	})
	let x = ref(4)

	console.log(obj.a)
	console.log(obj.b)
	console.log(obj.c)
	console.log(x) */
	

	// 数据
	let house = ref(4)
	// 方法
	function changeToy(){
		c1.value.toy = '小猪佩奇'
	}
	function changeComputer(){
		c2.value.computer = '华为'
	}
	function getAllChild(refs:{[key:string]:any}){
		console.log(refs)
		for (let key in refs){
			refs[key].book += 3
		}
	}
	// 向外部提供数据
	defineExpose({house})
</script>

子组件1:

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

<script setup lang="ts" name="Child1">
	import { ref } from "vue";
	// 数据
	let toy = ref('奥特曼')
	let book = ref(3)

	// 方法
	function minusHouse(parent:any){
		parent.house -= 1
	}

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

</script>

子组件2:

<template>
  <div class="child2">
    <h3>子组件2</h3>
		<h4>电脑:{{ computer }}</h4>
		<h4>书籍:{{ book }} 本</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
		import { ref } from "vue";
		// 数据
		let computer = ref('联想')
		let book = ref(6)
		// 把数据交给外部
		defineExpose({computer,book})
</script>

要让父组件能访问到子组件的信息,要将对应的数据使用defineExpose暴露出去。

同时,要让子组件能访问到父组件的信息,也要将对应的数据用defineExpose暴露出去。

方式七:provide-inject

provide-inject可以用于祖孙之间传递数据。provide传递数据使用k-v的形式,第一个参数写key,第二个参数写value。inject获取数据的时候,第一个参数指定要获取的key,第二个参数可以指定默认值,指定默认值后,可以使得ts校验直接通过默认值的格式去判断。

例:

父组件:

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

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

  let money = ref(100)
  let car = reactive({
    brand:'奔驰',
    price:100
  })
  function updateMoney(value:number){
    money.value -= value
  }

  // 向后代提供数据
  provide('moneyContext',{money,updateMoney})
  provide('car',car)

</script>

子组件:

<template>
  <div class="child">
    <h3>我是子组件</h3>
    <GrandChild/>
  </div>
</template>

<script setup lang="ts" name="Child">
  import GrandChild from './GrandChild.vue'
</script>

孙组件:

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>银子:{{ money }}</h4>
    <h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4>
    <button @click="updateMoney(6)">花爷爷的钱</button>
  </div>
</template>

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

  let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})
  let car = inject('car',{brand:'未知',price:0})
</script>

方式八:pinia

见以上关于pinia的内容

方式九:插槽

默认插槽

直接在子组件的双标签中,写上页面结构,此页面结构读取父组件的值。子组件中的页面结构上写上slot标签,如此包在子组件的双标签中的页面结构就会替代掉slot标签。例:

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Category title="热门游戏列表">
        <ul>
          <li v-for="g in games" :key="g.id">{{ g.name }}</li>
        </ul>
      </Category>
      <Category title="今日美食城市">
        <img :src="imgUrl" alt="">
      </Category>
      <Category title="今日影视推荐">
        <video :src="videoUrl" controls></video>
      </Category>
    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Category from './Category.vue'
  import { ref,reactive } from "vue";

  let games = reactive([
    {id:'asgytdfats01',name:'英雄联盟'},
    {id:'asgytdfats02',name:'王者农药'},
    {id:'asgytdfats03',name:'红色警戒'},
    {id:'asgytdfats04',name:'斗罗大陆'}
  ])
  let imgUrl = ref('<https://z1.ax1x.com/2023/11/19/piNxLo4.jpg>')
  let videoUrl = ref('<http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4>')

</script>

子组件:

<template>
  <div class="category">
    <h2>{{title}}</h2>
    <slot>默认内容</slot>
  </div>
</template>

<script setup lang="ts" name="Category">
  defineProps(['title'])
</script>

具名插槽

使用v-slot属性,并指定一个slot的名称,然后加在 组件标签 或者 template标签 上,使用 v-slot:名称 的方式也可以简写为#名称。接着子组件中,插槽slot标签指定下name属性,写上此插槽的名称。便会根据名称匹配插槽。默认插槽的名称为default。

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Category>
        <template v-slot:s2>
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </template>
        <template v-slot:s1>
          <h2>热门游戏列表</h2>
        </template>
      </Category>

      <Category>
        <template v-slot:s2>
          <img :src="imgUrl" alt="">
        </template>
        <template v-slot:s1>
          <h2>今日美食城市</h2>
        </template>
      </Category>

      <Category>
        <template #s2>
          <video video :src="videoUrl" controls></video>
        </template>
        <template #s1>
          <h2>今日影视推荐</h2>
        </template>
      </Category>
    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Category from './Category.vue'
  import { ref,reactive } from "vue";

  let games = reactive([
    {id:'asgytdfats01',name:'英雄联盟'},
    {id:'asgytdfats02',name:'王者农药'},
    {id:'asgytdfats03',name:'红色警戒'},
    {id:'asgytdfats04',name:'斗罗大陆'}
  ])
  let imgUrl = ref('<https://z1.ax1x.com/2023/11/19/piNxLo4.jpg>')
  let videoUrl = ref('<http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4>')

</script>

子组件:

<template>
  <div class="category">
    <slot name="s1">默认内容1</slot>
    <slot name="s2">默认内容2</slot>
  </div>
</template>

作用域插槽

如果要使用插槽,而且不是使用的父组件的数据,而是使用子组件的数据,就要使用作用域插槽。

比如在父组件的插槽部分的template标签或组件标签上,加上v-slot=”xxx”,子组件的slot标签上加上props属性,比如此props写作 :game=“games”,那么这个games变量的值,就会放到xxx的game属性中。

例:

父组件

<template>
  <div class="father">
    <h3>父组件</h3>
    <div class="content">
      <Game>
        <template v-slot="params">
          <ul>
            <li v-for="y in params.youxi" :key="y.id">
              {{ y.name }}
            </li>
          </ul>
        </template>
      </Game>

      <Game>
        <template v-slot="params">
          <ol>
            <li v-for="item in params.youxi" :key="item.id">
              {{ item.name }}
            </li>
          </ol>
        </template>
      </Game>

      <Game>
        <template #default="{youxi}">
          <h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
        </template>
      </Game>

    </div>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Game from './Game.vue'
</script>

子组件

<template>
  <div class="game">
    <h2>游戏列表</h2>
    <slot :youxi="games" x="哈哈" y="你好"></slot>
  </div>
</template>

<script setup lang="ts" name="Game">
  import {reactive} from 'vue'
  let games = reactive([
    {id:'asgytdfats01',name:'英雄联盟'},
    {id:'asgytdfats02',name:'王者农药'},
    {id:'asgytdfats03',name:'红色警戒'},
    {id:'asgytdfats04',name:'斗罗大陆'}
  ])
</script>

作用域插槽通常用于,数据在子组件,而结构是由父组件决定的。

如果作用域插槽也是具名的,那么使用v-slot:名称=”xxx”,来指定具名插槽即可