Vue3学习笔记(3)

pinia

创建及使用

main.js文件:

import { createApp } from 'vue'
import App from './App.vue'

/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'

/* 创建pinia */
const pinia = createPinia()
const app = createApp(App)

/* 使用插件 */{}
app.use(pinia)
app.mount('#app')

存储+读取数据

创建store文件夹(保存状态、业务逻辑的实体,每个组件都可以读取、写入它。它有三个概念:state、getter、action,相当于组件中的: datacomputedmethods。)

count.ts文件:

// 引入defineStore用于创建store
import {defineStore} from 'pinia'

// 定义并暴露一个store
export const useCountStore = defineStore('count',{
  // 动作
  actions:{},
  // 状态
  state(){
    return {
      sum:6
    }
  },
  // 计算
  getters:{}
})

talk.ts文件:

// 引入defineStore用于创建store
import {defineStore} from 'pinia'

// 定义并暴露一个store
export const useTalkStore = defineStore('talk',{
  // 动作
  actions:{},
  // 状态
  state(){
    return {
      talkList:[
        {id:'yuysada01',content:'你今天有点怪,哪里怪?怪好看的!'},
     		{id:'yuysada02',content:'草莓、蓝莓、蔓越莓,你想我了没?'},
        {id:'yuysada03',content:'心里给你留了一块地,我的死心塌地'}
      ]
    }
  },
  // 计算
  getters:{}
})

使用state中的数据:

<template>
	<ul>
    <li v-for="talk in talkStore.talkList" :key="talk.id">
      {{ talk.content }}
    </li>
  </ul>
</template>

<script setup lang="ts" name="Count">
  import axios from 'axios'
  import {useTalkStore} from '@/store/talk'

  const talkStore = useTalkStore()
</script><template>
  <h2>当前求和为:{{ sumStore.sum }}</h2>
</template>

<script setup lang="ts" name="Count">
  // 引入对应的useXxxxxStore
  import {useSumStore} from '@/store/sum'

  // 调用useXxxxxStore得到对应的store
  const sumStore = useSumStore()
</script>

修改数据

直接修改:

countStore.sum = 666

批量修改:

借助 `action`修改(`action`中可以编写一些业务逻辑)countStore.$patch({
  sum:999,
  school:'atguigu'
})

借助 action修改(`action`中可以编写一些业务逻辑):

组件中调用 `action`即可import { defineStore } from 'pinia'

export const useCountStore = defineStore('count', {
  /*************/
  actions: {
    //加
    increment(value:number) {
      if (this.sum < 10) {
        //操作countStore中的sum
        this.sum += value
      }
    },
    //减
    decrement(value:number){
      if(this.sum > 1){
        this.sum -= value
      }
    }
  },
  /*************/
})

组件中调用 action即可

// 使用countStore
const countStore = useCountStore()

// 调用对应action
countStore.incrementOdd(n.value)

storeToRefs

-借助 storeToRefsstore中的数据转为 ref对象,方便在模板中使用。

注意:pinia提供的 storeToRefs只会将数据做转换,而 VuetoRefs会转换 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>

getters

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

追加getters配置。

// 引入defineStore用于创建store
import {defineStore} from 'pinia'

// 定义并暴露一个store
export const useCountStore = defineStore('count',{
  // 动作
  actions:{
    /************/
  },
  // 状态
  state(){
    return {
      sum:1,
      school:'atguigu'
    }
  },
  // 计算
  getters:{
    bigSum:(state):number => state.sum *10,
    upperSchool():string{
      return this. school.toUpperCase()
    }
  }
})

组件中读取数据:

const {increment,decrement} = countStore
let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)

$subscribe

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

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

store组合式写法

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'

export const useTalkStore = defineStore('talk',()=>{
  // talkList就是state
  const talkList = reactive(
    JSON.parse(localStorage.getItem('talkList') as string) || []
  )

  // getATalk函数相当于action
  async function getATalk(){
    // 发请求,下面这行的写法是:连续解构赋值+重命名
    let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
    // 把请求回来的字符串,包装成一个对象
    let obj = {id:nanoid(),title}
    // 放到数组中
    talkList.unshift(obj)
  }
  return {talkList,getATalk}
})

组件通信

Vue3组件通信和 Vue2的区别:

移出事件总线,使用 mitt代替。

vuex换成了 pinia

.sync优化到了 v-model里面了。

$listeners所有的东西,合并到 $attrs中了。

$children被砍掉了。

props

概述:props是使用频率最高的一种通信方式,常用于 :父 ↔ 子。

父传子:属性值是非函数。

子传父:属性值是函数。

父组件:

<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>我的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

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

子组件:

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

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奥特曼')

	defineProps(['car','getToy'])
</script>

自定义事件

1. 概述:自定义事件常用于:子 => 父。

2. 注意区分好:原生事件、自定义事件。

原生事件:

事件名是特定的(click、mosueenter等等)

事件对象 $event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)

自定义事件:

事件名是任意名称

<strong style="color:red">事件对象 $event: 是调用 emit时所提供的数据,可以是任意类型!!!</strong >

<!--在父组件中,给子组件绑定自定义事件:-->
<Child @send-toy="toy = $event"/>

<!--注意区分原生事件与自定义事件中的$event-->
<button @click="toy = $event">测试</button>
//子组件中,触发事件:
this.$emit('send-toy', 具体数据)

mitt

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

emitter.ts文件:

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

提供数据的组件,在合适的时候触发事件

import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

注意这个重要的内置关系,总线依赖着这个内置关系

v-model

概述:实现 父↔子之间相互通信。

前序知识 —— v-model的本质

<!-- 使用v-model指令 -->
<input type="text" v-model="userName">

<!-- v-model的本质是下面这行代码 -->
<input 
  type="text" 
  :value="userName" 
  @input="userName =(<HTMLInputElement>$event.target).value"
>

组件标签上的 v-model的本质::moldeValue+ update:modelValue事件。

<!-- 组件标签上使用v-model指令 -->
<AtguiguInput v-model="userName"/>

<!-- 组件标签上v-model的本质 -->
<AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>

AtguiguInput组件中:

<template>
  <div class="box">
    <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
		<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
    <input 
       type="text" 
       :value="modelValue" 
       @input="emit('update:model-value',$event.target.value)"
    >
  </div>
</template>

<script setup lang="ts" name="AtguiguInput">
  // 接收props
  defineProps(['modelValue'])
  // 声明事件
  const emit = defineEmits(['update:model-value'])
</script>

也可以更换 value,例如改成 abc

`AtguiguInput`组件中:<!-- 也可以更换value,例如改成abc-->
<AtguiguInput v-model:abc="userName"/>

<!-- 上面代码的本质如下 -->
<AtguiguInput :abc="userName" @update:abc="userName = $event"/>

AtguiguInput组件中:

<template>
  <div class="box">
    <input 
       type="text" 
       :value="abc" 
       @input="emit('update:abc',$event.target.value)"
    >
  </div>
</template>

<script setup lang="ts" name="AtguiguInput">
  // 接收props
  defineProps(['abc'])
  // 声明事件
  const emit = defineEmits(['update:abc'])
</script>

如果 value可以更换,那么就可以在组件标签上多次使用 v-model

<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>

$attrs

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

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

注意:$attrs会自动排除 props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

<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用于 :父→子。

$parent用于:子→父。

原理:

父组件:

<template>
	<div class="father">
		<h3>父组件</h3>
		<h4>房产:{{ house }}</h4>
		<button @click="changeToy">修改Child1的玩具</button>
		<button @click="changeComputer">修改Child2的电脑</button>
		<!-- 获取所有儿子导出的数据,以对象的形式输出 -->
		<!-- refs为getAllChild的参数 -->
		<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(){//要得到孩子的数据需要孩子defineExpose
		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>

<style scoped>
	.father {
		background-color: rgb(165, 164, 164);
		padding: 20px;
		border-radius: 10px;
	}

	.father button {
		margin-bottom: 10px;
		margin-left: 10px;
	}
</style>

子组件1:

<template>
  <div class="child1">
    <h3>子组件1</h3>
		<h4>玩具:{{ toy }}</h4>
		<h4>书籍:{{ book }} 本</h4>
		<!-- parent为minusHouse函数的参数 -->
		<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>

<style scoped>
	.child1{
		margin-top: 20px;
		background-color: skyblue;
		padding: 20px;
		border-radius: 10px;
    box-shadow: 0 0 10px black;
	}
</style>

子组件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>

<style scoped>
	.child2{
		margin-top: 20px;
		background-color: orange;
		padding: 20px;
		border-radius: 10px;
    box-shadow: 0 0 10px black;
	}
</style>

provide&inject

概述:实现祖孙组件直接通信

具体使用:

在祖先组件中通过 provide配置向后代组件提供数据

在后代组件中通过 inject配置来声明接收数据

父组件中,使用 provide提供数据:

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>资产:{{ money }}</h4>
    <h4>汽车:{{ car }}</h4>
    <button @click="money += 1">资产+1</button>
    <button @click="car.price += 1">汽车价格+1</button>
    <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
  })
  // 用于更新money的方法
  function updateMoney(value:number){
    money.value += value
  }
  // 提供数据
  provide('moneyContext',{money,updateMoney})
  provide('car',car)
</script>

注意:子组件中不用编写任何东西,是不受到任何打扰的

孙组件中使用 inject配置项接受数据:

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>资产:{{ money }}</h4>
    <h4>汽车:{{ car }}</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:(x:number)=>{}})
  let car = inject('car')

slot(插槽)

默认插槽

效果:

代码呈现:

父组件中:
        <Category title="今日热门游戏">
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <!-- 默认插槽 -->
            <slot></slot>
          </div>
        </template>

具名插槽

父组件中:
        <Category title="今日热门游戏">
          <template v-slot:s1>
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template #s2>
            <a href="">更多</a>
          </template>
        </Category>
子组件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>
            <slot name="s2"></slot>
          </div>
        </template>

作用域插槽

理解:<span style="color:red">数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在 News组件中,但使用数据所遍历出来的结构由 App组件决定)

父组件中:
      <Game v-slot="params">
      <!-- <Game v-slot:default="params"> -->
      <!-- <Game #default="params"> -->
        <ul>
          <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
        </ul>
      </Game>

子组件中:
      <template>
        <div class="category">
          <h2>今日游戏榜单</h2>
          <slot :games="games" a="哈哈"></slot>
        </div>
      </template>

      <script setup lang="ts" name="Category">
        import {reactive} from 'vue'
        let games = reactive([
          {id:'asgdytsa01',name:'英雄联盟'},
          {id:'asgdytsa02',name:'王者荣耀'},
          {id:'asgdytsa03',name:'红色警戒'},
          {id:'asgdytsa04',name:'斗罗大陆'}
        ])
      </script>

其他API

shallowRef&shallowReactive

优点:

通过使用shallowRef()和 shallowReactive()来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。

shallowRef

作用:创建一个响应式数据,但只对顶层属性进行响应式处理。

用法:

特点:只跟踪引用值的变化,不关心值内部的属性变化。let myVar = shallowRef(initialValue);

特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

用法:

const myObj = shallowReactive({ ... });

特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

readonly与shallowReadonly

readonly

作用:用于创建一个对象的深只读副本。

用法:

* 特点:
  * 对象的所有嵌套属性都将变为只读。
  * 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
* 应用场景:
  * 创建不可变的状态快照。
  * 保护全局状态或配置不被修改。const original = reactive({ ... });
const readOnlyCopy = readonly(original);

特点:

对象的所有嵌套属性都将变为只读。

任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。

应用场景:

创建不可变的状态快照。

保护全局状态或配置不被修改。

shallowReadonly

作用:与 readonly 类似,但只作用于对象的顶层属性。

用法:

const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);

特点:

只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

适用于只需保护对象顶层属性的场景。

toRaw&markRaw

toRaw

作用:用于获取一个响应式对象的原始对象, toRaw 返回的对象不再是响应式的,不会触发视图更新。

官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

何时使用? —— 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

import { reactive,toRaw,markRaw,isReactive } from "vue";

/* toRaw */
// 响应式对象
let person = reactive({name:'tony',age:18})
// 原始对象
let rawPerson = toRaw(person)


/* markRaw */
let citysd = markRaw([
  {id:'asdda01',name:'北京'},
  {id:'asdda02',name:'上海'},
  {id:'asdda03',name:'天津'},
  {id:'asdda04',name:'重庆'}
])
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
let citys2 = reactive(citys)
console.log(isReactive(person))
console.log(isReactive(rawPerson))
console.log(isReactive(citys))
console.log(isReactive(citys2))

markRaw

作用:标记一个对象,使其永远不会变成响应式的。

例如使用 mockjs时,为了防止误把 mockjs变为响应式对象,可以使用 markRaw 去标记 mockjs

/* markRaw */
let citys = markRaw([
  {id:'asdda01',name:'北京'},
  {id:'asdda02',name:'上海'},
  {id:'asdda03',name:'天津'},
  {id:'asdda04',name:'重庆'}
])
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
let citys2 = reactive(citys)

customRef

  1. 定义

    • customRef是 Vue 3 中的一个高级特性。它用于自定义一个响应式引用(ref)。通常情况下,ref在 Vue 3 中用于创建基本的响应式数据,例如const count = ref(0),当count.value发生变化时,与之绑定的 DOM 部分会自动更新。但是customRef允许你更精细地控制这种响应式行为。

  2. 使用方式

    • customRef是一个函数,它接收一个工厂函数作为参数。这个工厂函数又接收tracktrigger这两个函数作为参数,并且返回一个包含getset方法的对象。

应用场景
防抖和节流:就像上面的例子一样,在处理用户输入(如搜索框输入)等场景中,可以使用customRef来实现防抖,减少不必要的更新。节流场景下可以控制在一定时间内只更新一次值。
异步数据获取和更新:如果数据的获取或者更新是异步的,例如从 API 获取数据后更新引用的值,可以通过customRef来管理这个过程,确保 DOM 在数据更新后正确地响应。
复杂的数据转换和验证:在设置值时,可以对新的值进行复杂的转换或者验证。比如,有一个输入框只允许输入数字,在set方法中可以对输入的值进行验证,如果不是数字就不进行更新或者进行转换后再更新。import { customRef } from 'vue';
export default {
    setup() {
        function useDebouncedRef(value, delay = 200) {
            let timeout;
            return customRef((track, trigger) => {
                return {
                    get() {
                        track();
                        return value;
                    },
                    set(newValue) {
                        clearTimeout(timeout);
                        timeout = setTimeout(() => {
                            value = newValue;
                            trigger();
                        }, delay);
                    }
                };
            });
        }
        const debouncedInput = useDebouncedRef('');
        return {
            debouncedInput
        };
    }
}
  1. 应用场景

    • 防抖和节流:就像上面的例子一样,在处理用户输入(如搜索框输入)等场景中,可以使用customRef来实现防抖,减少不必要的更新。节流场景下可以控制在一定时间内只更新一次值。

    • 异步数据获取和更新:如果数据的获取或者更新是异步的,例如从 API 获取数据后更新引用的值,可以通过customRef来管理这个过程,确保 DOM 在数据更新后正确地响应。

    • 复杂的数据转换和验证:在设置值时,可以对新的值进行复杂的转换或者验证。比如,有一个输入框只允许输入数字,在set方法中可以对输入的值进行验证,如果不是数字就不进行更新或者进行转换后再更新。


Vue3学习笔记(3)
http://localhost:8090//archives/vue3xue-xi-bi-ji-3
作者
林依琪
发布于
2024年11月25日
许可协议