共计 11585 个字符,预计需要花费 29 分钟才能阅读完成。
学习之前内容:Vue 3 快速上手学习笔记
pinia
搭建 pinia 环境
第一步:pnpm install pinia
第二步:修改 src/main.ts
import {createApp} from 'vue'
import App from './App.vue'
import {createPinia} from 'pinia'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
存储 + 读取 + 修改数据
Store 是一个保存 状态、业务逻辑 的实体,每个组件都可以读取、写入它。它有三个概念:state
、getter
、action
,相当于组件中的:data
、computed
和 methods
。
新建 src/store/sum.ts
import {defineStore} from 'pinia'
// 定义并暴露一个 store
export const useSumStore = defineStore('sum', {
// 状态
state() {
return {
sum: 9,
school: 'Hello',
}
},
// 计算
getters: {},
// 里面放方法,用于响应组件中的“动作”actions: {increment(value: number) {if (this.sum < 10) {this.sum += value // this 就是当前的 store}
},
},
})
修改 src\App.vue
<template>
<Sum />
</template>
<script setup lang="ts" name="App">
import Sum from './components/Sum.vue'
</script>
新建 src\components\Sum.vue
<template>
<div class="sum">
<h2>sum:{{sumStore.sum}}</h2>
<h3>school:{{school}}</h3>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add"> 加 </button>
<button @click="minus"> 减 </button>
</div>
</template>
<script setup lang="ts" name="Count">
import {ref} from 'vue'
import {useSumStore} from '@/store/sum'
const sumStore = useSumStore()
// 数据
let n = ref(1) // 用户选择的数字
// 方法
function add() {
// 第一种修改方式
// sumStore.sum += 1 // sumStore.$state.sum 也能拿到 state 中的数据
// 第二种修改方式:批量修改
// sumStore.$patch({
// sum: 666,
// school: 'abc',
// })
// 第三种修改方式
sumStore.increment(n.value)
}
function minus() {sumStore.sum -= n.value}
</script>
<style scoped>
.sum {
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,
button {
margin: 0 5px;
height: 25px;
}
</style>
storeToRefs
借助 storeToRefs 将 store 中的数据转为 ref 对象,方便在模板中使用。注意 pinia 提供的 storeToRefs 只会将数据做转换,而 Vue 的 toRefs 会转换 store 中所有数据(包括方法)。
修改 src\components\Sum.vue
<template>
<div class="sum">
<h2>sum:{{sum}}</h2>
<h3>school:{{school}}</h3>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add"> 加 </button>
<button @click="minus"> 减 </button>
</div>
</template>
<script setup lang="ts" name="Count">
import {ref} from 'vue'
import {storeToRefs} from 'pinia'
import {useSumStore} from '@/store/sum'
const sumStore = useSumStore()
// storeToRefs 只关注 sotre 中数据,不会对方法进行 ref 包裹
const {sum, school} = storeToRefs(sumStore)
let n = ref(1) // 用户选择的数字
function add() {sumStore.increment(n.value)
}
function minus() {sumStore.sum -= n.value}
</script>
getters
当 state 中的数据,需要经过处理后再使用时,可以使用 getters 配置。
修改 getters 配置 src\store\sum.ts
getters: {bigSum: (state): number => state.sum * 10,
upperSchool(): string {return this.school.toUpperCase()
},
},
修改 src\components\Sum.vue
<template>
<div class="sum">
<h2>sum:{{sum}},放大 10 倍:{{bigSum}}</h2>
<h3>school:{{school}},大写:{{upperSchool}}</h3>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add"> 加 </button>
<button @click="minus"> 减 </button>
</div>
</template>
<script setup lang="ts" name="Count">
import {ref} from 'vue'
import {storeToRefs} from 'pinia'
import {useSumStore} from '@/store/sum'
const sumStore = useSumStore()
const {sum, school, bigSum, upperSchool} = storeToRefs(sumStore)
let n = ref(1)
function add() {sumStore.increment(n.value)
}
function minus() {sumStore.sum -= n.value}
</script>
$subscribe
通过 store 的 $subscribe()方法侦听 state 及其变化。
修改 src\components\Sum.vue
sumStore.$subscribe((mutate, state) => {console.log('sumStore', mutate, state)
})
store 组合式写法
新建 src\store\talkList.ts
import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'
export const useTalkStore = defineStore('talkList', () => {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}
})
修改 src\App.vue
<template>
<TalkList />
</template>
<script setup lang="ts" name="App">
import TalkList from './components/TalkList.vue'
</script>
新建 src\components\TalkList.vue
<template>
<div class="talk">
<button @click="getLoveTalk"> 获取一句土味情话 </button>
<ul>
<li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li>
</ul>
</div>
</template>
<script setup lang="ts" name="LoveTalk">
import {useTalkStore} from '@/store/talkList'
import {storeToRefs} from 'pinia'
const talkStore = useTalkStore()
const {talkList} = storeToRefs(talkStore)
talkStore.$subscribe((mutate, state) => {localStorage.setItem('talkList', JSON.stringify(state.talkList))
})
// 方法
function getLoveTalk() {talkStore.getATalk()
}
</script>
<style scoped>
.talk {
background-color: orange;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
</style>
组件通信
props
props 是使用频率最高的一种通信方式,常用于父子通信:
- 父传子:属性值非函数
- 子传父:属性值是函数
修改 src\App.vue
<template>
<Father />
</template>
<script setup lang="ts" name="App">
import Father from './components/Father.vue'
</script>
新建 src\components\Father.vue
<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 '@/components/Child.vue'
import {ref} from 'vue'
// 数据
let car = ref('奔驰')
let toy = ref('')
// 方法
function getToy(value: string) {toy.value = value}
</script>
<style scoped>
.father {background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
</style>
新建 src\components\Child.vue
<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>
<style scoped>
.child {
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
</style>
自定义事件
常用于子传父,注意区分原生事件、自定义事件:
-
原生事件:
- 事件名是特定的(click、mosueenter 等)
- 事件对象 $event: 包含事件相关信息的对象(pageX、pageY、target、keyCode)
-
自定义事件:
- 事件名可以任意名称
- 事件对象 $event: 是调用 emit 时所提供的数据,可以是任意类型
修改 src\components\Father.vue
<template>
<div class="father">
<h3> 父组件 </h3>
<h4 v-show="toy"> 子给的玩具:{{toy}}</h4>
<!-- 给子组件绑定事件 -->
<Child @send-toy="saveToy" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from '@/components/Child.vue'
import {ref} from 'vue'
// 数据
let toy = ref('')
// 用于保存传递过来的玩具
function saveToy(value: string) {console.log('saveToy', value)
toy.value = value
}
</script>
修改 src\components\Child.vue
<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>
$event 到底是啥?啥时候能.target:
- 原生事件,$event 就是事件对象,能 .target
- 自定义事件,$event 就是触发事件时,所传递的数据,不能 .target
mitt
与消息订阅与发布(pubsub)功能类似,实现任意组件间通信。
安装 mitt:pnpm i mitt
新建 src\utils\emitter.ts
import mitt from 'mitt'
const emitter = mitt()
// emitter.on() 绑定事件
// emitter.off() 解绑事件
// emitter.emit() 触发事件
// emitter.all.clear() 清理事件
export default emitter
在 main.ts 引入 emitter
import {createApp} from 'vue'
import App from './App.vue'
import emitter from './utils/emitter'
createApp(App).mount('#app')
src\App.vue
<template>
<Father />
</template>
<script setup lang="ts" name="App">
import Father from './components/Father.vue'
</script>
src\components\Father.vue
<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>
<style scoped>
.father {background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.father button {margin-left: 5px;}
</style>
提供数据组件 src\components\Child1.vue,在合适时候触发事件。
<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>
<style scoped>
.child1 {
margin-top: 50px;
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
.child1 button {margin-right: 10px;}
</style>
接收数据组件 src\components\Child2.vue:绑定事件、同时在卸载前解绑事件。
<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>
<style scoped>
.child2 {
margin-top: 50px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
</style>
v-model
v-model 用在 HTML 标签上:<input type="text" v-model="userName" />
,本质上是:<input type="text" :value="userName" @input="userName = (<HTMLInputElement>$event.target).value" />
。
v-model 用在组件标签上,本质上是::moldeValue + update:modelValue 事件。
<!-- 组件标签上使用 v -model 指令 -->
<AInput v-model="userName" />
<!-- 本质上 -->
<AInput :modelValue="userName" @update:modelValue="userName = $event" />
可以在组件标签上多次使用 v -model:<AInput v-model:usr="userName" v-model:pwd="password" />
。
$attrs
用于实现当前组件的父组件,向当前组件的子组件通信,即祖传孙。
$attrs 是一个对象,包含所有父组件传入的标签属性。$attrs 会自动排除 props 中声明的属性(可认为声明过的 props 被子组件自己“消费”了)。
父组件 src\components\Father.vue:
<template>
<div class="father">
<h3> 父组件 </h3>
<h4>a:{{a}}</h4>
<h4>b:{{b}}</h4>
<Child :a="a" :b="b" 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)
function updateA(value: number) {a.value += value}
</script>
子组件 src\components\Child.vue:
<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>x:{{x}}</h4>
<h4>y:{{y}}</h4>
<button @click="updateA(3)"> 点我更新爷爷那的 a </button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a', 'b', 'x', 'y', 'updateA'])
</script>
$refs 和 $parent
$refs 用于:父 → 子,$parent 用于:子 → 父。
- $refs 值为对象,包含所有被 ref 属性标识的 DOM 元素或组件实例
- $parent 值为对象,当前组件的父组件实例对象
src\components\Father.vue
<template>
<div class="father">
<h3> 父组件 </h3>
<h4> 房产:{{house}}</h4>
<button @click="changeToy"> 修改孩子的玩具 </button>
<button @click="getAllChild($refs)"> 让孩子的书变多 </button>
<Child ref="c" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import {ref, reactive} from 'vue'
let c = ref()
let house = ref(4)
function changeToy() {c.value.toy = '小猪佩奇'}
function getAllChild(refs: { [key: string]: any }) {console.log(refs)
for (let key in refs) {refs[key].book += 3
}
}
// 向外部提供数据
defineExpose({house})
</script>
src\components\Child.vue
<template>
<div class="child">
<h3> 子组件 </h3>
<h4> 玩具:{{toy}}</h4>
<h4> 书籍:{{book}} 本 </h4>
<button @click="minusHouse($parent)"> 父亲房产 -1</button>
</div>
</template>
<script setup lang="ts" name="Child">
import {ref} from 'vue'
// 数据
let toy = ref('奥特曼')
let book = ref(3)
// 方法
function minusHouse(parent: any) {parent.house -= 1}
// 把数据交给外部
defineExpose({toy, book})
</script>
provide 和 inject
实现祖孙组件直接通信,具体使用:
- 在祖先组件中通过 provide 配置向后代组件提供数据
- 在后代组件中通过 inject 配置来声明接收数据
src\components\Father.vue
<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: 200,
})
function updateMoney(value: number) {money.value -= value}
// 向后代提供数据
provide('moneyContext', { money, updateMoney})
provide('car', car)
</script>
src\components\Child.vue
<template>
<div class="child">
<h3> 子组件 </h3>
<GrandChild />
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
src\components\GrandChild.vue
<template>
<div class="grand-child">
<h3> 孙组件 </h3>
<h4> 钱:{{money}}万元 </h4>
<h4> 车:一辆 {{car.brand}},价值{{car.price}} 万 </h4>
<button @click="updateMoney(3)"> 花爷爷的钱 </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>