Vue3 & Pinia
Vue3 & Pinia
以下为学习过程中的极简提炼笔记,以供重温巩固学习
学习准备
准备工作
学完vue2
学习目的
Vue3
为什么要学 Vue3
- vue3已成为vue的默认版本
Vue3的优势
更容易维护
- 组合式API
- 更好的TypeScript支持(Vue3源码通过TypeScript重写,TypeScript是JavaScript的超集)
更快的速度
- 重写diff算法
- 模版编译优化
- 更高效的组件初始化
更小的体积
- 良好的TreeShaking
- 按需引入
更优的数据响应式
- Proxy底层代理,针对对象代理,对对象新增或减少属性,均不影响对象的数据劫持
一、TypeScript与JavaScript的主要区别
类型系统:JavaScript是一种动态类型语言,变量的类型是在运行时确定的。而TypeScript则是一种静态类型语言,它在编译阶段就确定了变量的类型,并提供了强大的类型系统,包括基础类型、联合类型、交叉类型等。
编译过程:JavaScript是一种解释型语言,源代码在浏览器中直接执行。而TypeScript则需要先通过TypeScript编译器(tsc)编译成JavaScript代码,然后再在浏览器中执行。
面向对象编程:虽然JavaScript也支持面向对象编程,但TypeScript的面向对象特性更为丰富,它支持类(Class)、接口(Interface)、泛型(Generics)等概念。
工具支持:TypeScript由于其强大的类型系统和编译过程,得到了众多开发工具的支持,如自动补全、接口提示、错误检查等,极大地提高了开发效率。
二、TypeScript与JavaScript的使用场景
⭕大型项目:对于规模较大、复杂度较高的项目,TypeScript的优势尤为明显。其强大的类型系统和编译过程有助于减少运行时错误,提高代码的可维护性和可重用性。同时,TypeScript的面向对象特性和工具支持也使得大型项目的开发更加高效。
✨团队协作:在团队协作中,TypeScript的规范性和工具支持有助于统一代码风格、减少沟通成本。此外,TypeScript的类型系统也有助于团队成员更好地理解代码意图,提高代码的可读性和可维护性。
与后端语言配合:在许多情况下,前端项目需要与后端语言(如Java、C#等)进行交互。由于TypeScript的类型系统更加严格和规范,因此与后端语言的配合更加顺畅,有助于减少数据传输和转换过程中的错误。
JavaScript库和框架的开发:许多知名的JavaScript库和框架(如React、Angular等)都使用TypeScript进行开发。这主要是因为TypeScript的类型系统和工具支持有助于提高库和框架的稳定性和可扩展性。
对比Vue2 选项式 API vs Vue3 组合式API
Vue2 选项式options API
- 定义:在整个配置项中,有一个个选项属性,如
data(){}
数据,methods(){}
方法,computed(){}
计算属性,watch(){}
监听器等各个选项 - 特征:如需实现功能,实际上需要分散式地,将该功能的实现代码,散落到各个配置项中
- 定义:在整个配置项中,有一个个选项属性,如
Vue3 组合式composition API
- 效果:如,提供数据时,不再需要到data配置项中,而在调用方法时提供
- 通过将同一个功能相关的数据、调用方法、声明函数等,集合式管理,集中放到一起
- 功能集中式管理,便于复用,可以通过将功能封装成函数实现复用
- 在函数中声明数据、声明方法、声明计算属性等
- 在页面中通过直接调函数,实现使用

- 通过案例,体验选项式options API 与 组合式composition API的区别
- 声明数据:通过调方法的方式声明数据
const count = ref(0)
- 声明函数:点击调用函数时,让数据执行++
- 声明数据:通过调方法的方式声明数据

- Vue3 组合式composition API 的优点:
- 代码量变少了
- 分散式维护转为集中式维护,更易封装复用
<script>
export default {
data(){
return {
count:0
}
},
methods:{
addCount(){
this.count++
}
}
}
</script>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = ()=> count.value++
</script>
通过脚手架create-vue 搭建Vue3项目
- 认识 create-vue 脚手架
- create-vue是Vue官方新的脚手架工具,底层切换到了 vite(下一代构建工具),为开发提供极速响应
- Vue-CLI脚手架底层基于webpack
- create-vue脚手架底层基于vite

使用create-vue创建项目
基于create-vue脚手架创建项目
- 前提环境条件
- 已安装 16.0 或更高版本的 Node.js
node -v
查看Node.js版本
- 创建一个Vue应用
npm init vue@latest
或pnpm create vue@latest
这一指令将会安装并执行 create-vue- 小键盘上下左右选中配置项,回车确认
熟悉项目目录和关键文件

- 关键文件:
vite.config.js - 项目的配置文件 基于vite的配置
- alias,别名配置项,
'@': fileURLToPath(new URL('./src', import.meta.url))
允许通过'@'
别名访问'./src'
目录 - vite配置官方文档https://vite.dev/config/
- alias,别名配置项,
package.json - 项目包文件 核心依赖项变成了 Vue3.x 和 vite
main.js - 入口文件 createApp函数创建应用实例
- main.js中,从vue导入函数
import { createApp } from 'vue'
,并基于函数创建vue - vue2:基于
new Vue()
创建一个应用实例 - vue3:基于
createApp()
创建一个应用实例,本质是对创建实例进行封装,实际上将应用实例封装成一个函数 - vue3对语法进行统一,创建实例
createApp()
、创建路由模块createRouter()
、创建仓库createStore()
,都是使用函数 - 对创建实例进行封装,好处是保证每个实例的独立性、封闭性;在中大型项目、多应用、多实例管理时,不会有相互干扰和影响
createApp(App).mount('#app')
,mount 设置挂载点 #app (id为app的盒子)
- main.js中,从vue导入函数
app.vue - 根组件 SFC单文件组件 script - template - style
- vetur是vue2插件;volar/Vue - Official是vue3插件
- 变化一:脚本script和模板template顺序调整,
<script setup>
逻辑第一,<template>
结构第二,<style scoped>
样式第三,结构和样式放一起,方便更改 - 变化二:模板template不再要求唯一根元素,允许多个根元素
- 变化三:脚本script添加setup标识,支持组合式API(加上setup允许在script中直接编写组合式API)
- 不需要组件注册,在
<script setup>
中导入后,即可在template中使用 <style scoped>
加上scoped,使得样式只在当前组件内起作用
index.html - 单页入口 提供id为app的挂载点
- 在main.js中
import App from './App.vue'
导入了单文件组件,并基于单文件组件createApp(App).mount('#app')
创建实例 createApp(App)
创建实例,.mount('#app')
为实例设置挂载点,#app (页面中id为app的盒子)- index.html为单页入口,提供id为app的挂载点,将来main.js中创建的内容和结构,会往
<div id="app"></div>
中挂载
- 在main.js中
{
"name": "vue3study",
"version": "0.0.0",
"private": true,
"type": "module",
// 核心依赖项变成了 Vue3.x 和 vite
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"vue": "^3.5.13"
},
// 核心依赖项变成了 Vue3.x 和 vite
"devDependencies": {
"@eslint/js": "^9.14.0",
"@vitejs/plugin-vue": "^5.2.1",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"vite": "^6.0.1",
"vite-plugin-vue-devtools": "^7.6.5"
}
}
组合式API - setup选项
- 组合式API:
- 组合式API可以理解为一系列函数
- 将来通过调用组合式API/一系列函数,去编写组件逻辑
- 写组件逻辑时,setup选项就是组合式API/一系列函数的入口
- 需要先写setup选项,才能往里面写组合式API函数
setup选项的写法和执行时机
- setup选项的写法
- 需要写成一个函数,与生命周期钩子函数类似,在export default配置项中写
- 写法:定义数据 + 函数 然后以对象方式return
- 将来在setup(){中},编写组合式API,在里面发起各种函数调用
<script>
export default {
setup(){
},
beforeCreate(){
}
}
</script>
- setup选项的执行时机
- 在beforeCreate钩子之前执行
- 即,在8个生命周期函数(钩子函数)之前执行
- 自动执行
setup选项的基本使用注意事项
- 因为setup选项在创建实例之前就执行,所以拿不到this(此时的this是undefined)
- 在vue3的组合式api开发中,尽量不使用this,因为this是基于当前环境指向当前的组件实例,this会依赖于当前的环境,不使用this会有利于封装组合式api
- 往setup中编写代码时,在setup中提供的任何数据和方法/函数,必须通过return后,才能到template模板中使用,才能应用在页面
- 通过
<script setup>
语法糖简化代码,简化后,在<script setup></script>
标签对包裹的任何数据和方法/函数,已默认return,可以直接在template模板中使用

<script>
// setup
// 1. 执行时机,比beforeCreate还要早
// 2. setup函数中,获取不到this (this是undefined)
// 3. 数据 和 函数,需要在 setup 最后 return,才能模板中应用
// 问题:每次都要return,好麻烦?
// 4. 通过 setup 语法糖简化代码
// 默认导出对象
export default {
// 各配置项
setup () {
// console.log('setup函数', this)此打印早于beforeCreate中的console.log('beforeCreate函数'),因此执行时机,比beforeCreate还要早,打印出来this是undefined
// 数据
const message = 'hello Vue3'
// 函数
const logMessage = () => {
console.log(message)
}
return {
message,
logMessage
}
},
beforeCreate () {
console.log('beforeCreate函数')
}
}
</script>
<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>
<script setup>
const message = 'this is a message'
const logMessage = () => {
console.log(message)
}
</script>
<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>
<script setup>
语法糖原理:后台帮你return
script标签添加 setup标记,经过语法糖的封装,不需要再写导出语句,默认会添加导出语句

- 总结:
- setup选项的执行时机?
- beforeCreate钩子之前 自动执行
- setup写代码的特点是什么?
- 定义数据 + 函数 然后以对象方式return
<script setup>
解决了什么问题?
- 经过语法糖的封装更简单的使用组合式API
- setup中的this还指向组件实例吗?
- 指向undefined
组合式API - reactive和ref函数
- 背景:
- 在vue中,数据的默认状态,并非是响应式数据
- 如需要让数据变为响应式数据,需要经过reactive和ref函数处理
reactive()
作用:接受 对象类型 数据的参数传入,并返回一个 响应式的对象
核心步骤/使用方法:
- 从 vue 包中导入 reactive 函数
import { reactive } from 'vue'
- 在
<script setup>
中执行 reactive 函数并传入类型为对象的初始值,并使用变量接收返回值- 即在
const ABCD = reactive({对象类型数据})
reactive包裹的对象类型数据,会变为响应式数据,并将响应式数据的结果赋值给到ABCD
- 即在
- 总结:即导包,调方法,把对象转换成响应式,赋值给当前变量接收
- 从 vue 包中导入 reactive 函数
reactive()语法
<script setup>
// 从 vue 包中导入 reactive 函数
import { reactive } from 'vue'
// 执行reactive函数 传入参数 变量接收
// const ABCD = reactive({对象类型数据})
const state = reactive({
msg:'this is msg'
})
const setSate = ()=>{
// 修改数据更新视图
state.msg = 'this is new msg'
}
</script>
<template>
{{ state.msg }}
<button @click="setState">change msg</button>
</template>
<script setup>
// 1. reactive: 接收一个对象类型的数据,返回一个响应式的对象
// 问题:如果是简单类型,怎么办呢?
import { reactive } from 'vue'
const state = reactive({
count: 100
})
// 声明函数去验证响应式
const setCount = () => {
state.count++
}
</script>
<template>
<div>
<div>{{ state.count }}</div>
<button @click="setCount">+1</button>
</div>
</template>
ref()
作用:接收 简单类型 或者 对象类型 的数据传入,并返回一个 响应式的对象
核心步骤/使用方法:
- 从 vue 包中导入 ref 函数
import { ref } from 'vue'
- 在
<script setup>
中执行 ref 函数并传入初始值,使用变量接收 ref 函数的返回值- 即在
const ABCD = ref(简单类型 或者 {复杂类型}数据)
ref包裹的简单类型 或者 复杂类型数据,会变为响应式数据,并将响应式数据的结果,返回一个 响应式的对象,赋值给到ABCD
- 即在
- 总结:即导包,调方法,把对象转换成响应式,赋值给当前变量接收,使用上与reactive()一致
- 本质:
- 是在原有传入数据的基础上,外层包了一层对象,数据变成了一个对象,包成了复杂类型底层;
- 包成复杂类型之后,再借助 reactive 实现的响应式,返回一个响应式对象给变量
- 注意点:
- 脚本中,即
<script setup></script>
中访问数据,需要通过 .value - 在template模板中/页面中,.value不需要加 (帮我们扒了一层)
- 脚本中,即
- 推荐:以后声明数据,统一用 ref => 统一了编码规范
- 从 vue 包中导入 ref 函数
ref()语法
<script setup>
// 从 vue 包中导入 ref 函数
import { ref } from 'vue'
// 执行ref函数 传入参数 变量接收
// const ABCD = ref(简单类型 或者 {复杂类型}数据),返回一个 响应式的对象
const count = ref(0)
const setCount = ()=>{
// 修改数据更新视图必须加上.value,此时的count是一个对象,对对象中的value值操作
count.value++
}
</script>
<template>
<button @click="setCount">{{count}}</button>
</template>
<script setup>
// 1. reactive: 接收一个对象类型的数据,返回一个响应式的对象
// 问题:如果是简单类型,怎么办呢?
// import { reactive } from 'vue'
// const state = reactive({
// count: 100
// })
// const setCount = () => {
// state.count++
// }
// 2. ref: 接收简单类型 或 复杂类型,返回一个响应式的对象
// 本质:是在原有传入数据的基础上,外层包了一层对象,包成了复杂类型
// 底层,包成复杂类型之后,再借助 reactive 实现的响应式
// 注意点:
// 1. 脚本中访问数据,需要通过 .value
// 2. 在template中,.value不需要加 (帮我们扒了一层)
// 推荐:以后声明数据,统一用 ref => 统一了编码规范
import { ref } from 'vue'
const count = ref(0)
const setCount = () => {
count.value++
}
</script>
<template>
<div>
<div>{{ count }}</div>
<button @click="setCount">+1</button>
</div>
</template>
- 总结:
- reactive和ref函数的共同作用是什么 ?
- 用函数调用的方式生成响应式数据
- reactive vs ref ?
- reactive不能处理简单类型的数据
- ref参数类型支持更好,但是必须通过.value访问修改
- ref函数的内部实现依赖于reactive函数
- 在实际工作中推荐使用哪个?
- 推荐使用ref函数,更加灵活统一
组合式API - computed
- vue2中的computed:作为一个配置项,在配置项中提供计算属性
- vue2的选项式配置,没有把实现同一个功能的配置作集中性管理
- vue3中的computed:需要计算属性时,通过调用函数,得到计算属性
- 符合组合式api的特性,允许任意组合代码逻辑
computed计算属性函数
计算属性基本思想和Vue2的完全一致,组合式API下的计算属性只是修改了写法
核心步骤:
- 导入computed函数
import { computed } from 'vue'
- 在任意需要计算属性的位置,调用函数
const ABCD = computed(()=>{return 计算属性的计算逻辑})
,基于响应式数据,通过计算逻辑作计算,返回计算结果值 - 传参是传递一个函数,函数是计算属性的计算逻辑
- 执行函数 在回调参数中return基于响应式数据做计算的值,用变量接收
- 支持往计算属性中传函数,也支持往计算属性中传对象,包括get、set对象
- 导入computed函数
computed计算属性函数语法
<script setup>
// 导入
import {ref, computed } from 'vue'
// 原始数据
const count = ref(0)
// 计算属性,执行函数,通过变量接收,在回调函数中return计算值
const doubleCount = computed(()=>count.value * 2)
// 原始数据
const list = ref([1,2,3,4,5,6,7,8])
// 计算属性list
const filterList = computed(item=>item > 2)
</script>
- 计算属性小案例
- 计算公式:始终从原始响应式数组中筛选出大于2的所有项 - filter
<script setup>
// const 计算属性 = computed计算函数(() => {
// return 计算返回的结果
// })
import { computed, ref } from 'vue'
// 声明数据
const list = ref([1, 2, 3, 4, 5, 6, 7, 8])
// list是一个对象
//console.log(list);
// list.value是一个数组
//console.log(list.value);
// 基于list派生一个计算属性,从list中过滤出 > 2
const computedList = computed(() => {
return list.value.filter(item => item > 2)
})
// 定义一个修改数组的方法
const addFn = () => {
list.value.push(666)
}
</script>
<template>
<div>
<div>原始数据: {{ list }}</div>
<div>计算后的数据: {{ computedList }}</div>
<button @click="addFn" type="button">修改</button>
</div>
</template>
- 最佳实践
- 计算属性中不应该有“副作用”
- 比如异步请求/修改dom,应该丢到watch中执行
- 避免直接修改计算属性的值
- 计算属性应该是只读的,特殊情况可以配置 get set(例如全选反选)
- 计算属性中不应该有“副作用”
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
组合式API - watch
watch函数
作用:
- 侦听一个或者多个数据的变化,数据变化时执行回调函数
俩个额外参数:
- immediate(立即执行)
- deep(深度侦听)
基础使用 - 侦听单个/多个数据
- 导入watch函数
- 执行watch函数侦听变化
- watch函数中,传入要侦听的响应式数据(ref对象)和回调函数
- 单个ref对象数据
- 或者数组
[ref对象1,ref对象2]
包裹多个ref对象
- 回调函数中传新值形参1和老值形参2
- 单个ref对象数据
(newValue, oldValue)
- 多个ref对象数据,新值形参1和老值形参2分别用数组包裹
([newValue1,newValue2], [oldValue1,oldValue2])
- 单个ref对象数据
说明:同时侦听多个响应式数据的变化,不管哪个数据变化都需要执行回调
watch函数语法侦听单个数据
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0)
// 2. 调用watch 侦听变化
watch(count, (newValue, oldValue)=>{
console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
})
</script>
- watch函数语法侦听多个数据
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('cp')
// 2. 调用watch 侦听变化
watch([count, name], ([newCount, newName],[oldCount,oldName])=>{
console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName]`)
})
</script>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const nickname = ref('张三')
const changeCount = () => {
count.value++
}
const changeNickname = () => {
nickname.value = '李四'
}
// 1. 监视单个数据的变化
// watch(ref对象, (newValue, oldValue) => { ... })
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const nickname = ref('张三')
const changeCount = () => {
count.value++
}
const changeNickname = () => {
nickname.value = '李四'
}
// 2. 监视多个数据的变化
// watch([ref对象1, ref对象2], (newArr, oldArr) => { ... })
watch([count, nickname], (newArr, oldArr) => {
console.log(newArr, oldArr)
})
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
</template>
immediate
说明:
- 通过在第三个参数的位置上,添加额外的配置项
immediate: true
,实现在侦听器创建时(如一进页面创建侦听器时)立即触发回调 - 响应式数据变化之后继续执行回调
- 通过在第三个参数的位置上,添加额外的配置项
watch函数配置immediate: true语法
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0)
// 2. 调用watch 侦听变化
watch(count, (newValue, oldValue)=>{
console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
},{
immediate: true
})
</script>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const nickname = ref('张三')
const changeCount = () => {
count.value++
}
const changeNickname = () => {
nickname.value = '李四'
}
// 3. immediate 立刻执行,一进页面创建侦听器时立刻执行一次
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, {
immediate: true
})
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
</template>
deep
默认机制:
- 通过watch监听的ref对象默认是浅层侦听的.value,直接修改嵌套的对象属性不会触发回调执行,需要开启deep选项
- 如果watch监听的ref对象传入的是复杂数据类型,比如对象,要在watch里面加
{deep:true}
- 开了
{deep:true}
,监视的对象中的所有子属性的变化/对象内部数据的变化,都能被监视到
watch函数配置deep: true语法
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state
watch(state, ()=>{
console.log('数据变化了')
})
const changeStateByCount = ()=>{
// 直接修改不会引发回调执行
state.value.count++
}
</script>
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state 并开启deep
watch(state, ()=>{
console.log('数据变化了')
},{deep:true})
const changeStateByCount = ()=>{
// 此时修改可以触发回调
state.value.count++
}
</script>
<script setup>
import { ref, watch } from 'vue'
// 简单数据类型数据
const count = ref(0)
const nickname = ref('张三')
const changeCount = () => {
count.value++
}
const changeNickname = () => {
nickname.value = '李四'
}
// 复杂数据类型数据
const userInfo = ref({
name: 'zs',
age: 18
})
const setUserInfo = () => {
// 修改了 userInfo.value 修改了对象的地址,才能监视到,
// 比如下面赋值一个新的对象,此时不开true都能监听到
// userInfo.value = { name: 'ls', age: 50 }
userInfo.value.age++
}
// =======上面提供数据,下面提供watch=====================
// 4. deep 深度监视, 默认 watch 进行的是 浅层监视
// const ref1 = ref(简单类型) 可以直接监视
// const ref2 = ref(复杂类型) 监视不到复杂类型内部数据的变化
// 4.1 deep 深度监视 语法 简单类型可以直接监视.value
// watch(count, (newValue, oldValue) => {
// console.log(newValue, oldValue)
// })
// 4.2 deep 深度监视 语法 复杂类型监视不到复杂类型内部数据的变化,需要开启deep:true选项
watch(userInfo, (newValue) => {
console.log(newValue)
}, {
deep: true
})
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
<!-- 下方为复杂类型数据 -->
<div>{{ userInfo }}</div>
<button @click="setUserInfo">修改userInfo</button>
</template>
精确侦听对象的某个/单个子属性
深度监听deep:true:相当于监听了ref对象中的所有子属性,任一子属性的变化/对象内部数据的变化,都能被监视到
需求:在不开启deep的前提下,侦听age的变化,只有age变化时才执行回调
方法:可以把watch函数的第一个参数,写成回调函数的写法,在回调函数中返回要监听的具体属性
watch函数在不开启deep的前提下,单独侦听复杂数据类型中的某个子属性的语法
const Info = ref({
name: 'zs',
age: 18
})
// 单独监听Info复杂数据类型中的某个子属性Info.value.age
watch(
// 将需要单独监听的子属性,写成一个回调函数() => ref对象.value.属性
// 也可以理解为,将watch的第一个参数,写成一个函数 `() => ref对象.value.属性`
() => Info.value.age,
() => console.log('age发生了变化')
)
<script setup>
import { ref, watch } from 'vue'
// 复杂数据类型数据
const userInfo = ref({
name: 'zs',
age: 18
})
const setUserInfo = () => {
// 修改了 userInfo.value 修改了对象的地址,才能监视到,
// 比如下面赋值一个新的对象,此时不开true都能监听到
// userInfo.value = { name: 'ls', age: 50 }
userInfo.value.age++
}
// 5. 对于对象中的单个属性,进行监视
watch(
() => userInfo.value.age,
(newValue, oldValue) => {console.log(newValue, oldValue)}
)
// 对比下方简单/复杂数据类型的深度监视的语法
// 简单数据类型数据
// const count = ref(0)
// const nickname = ref('张三')
// const changeCount = () => {
// count.value++
// }
// const changeNickname = () => {
// nickname.value = '李四'
// }
// 4.1 deep 深度监视 语法 简单类型可以直接监视.value
// watch(count, (newValue, oldValue) => {
// console.log(newValue, oldValue)
// })
// 4.2 deep 深度监视 语法 复杂类型监视不到复杂类型内部数据的变化,需要开启deep:true选项
// watch(userInfo, (newValue) => {
// console.log(newValue)
// }, {
// deep: true
// })
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
<!-- 下方为复杂类型数据 -->
<div>{{ userInfo }}</div>
<button @click="setUserInfo">修改userInfo</button>
</template>
- 总结:
- 作为watch函数的第一个参数,ref对象需要添加.value吗?
- 不需要,第一个参数就是传 ref 对象
- watch只能侦听单个数据吗?
- 单个 或者 多个(传数组)
- 不开启deep,直接监视 复杂类型,修改属性 能触发回调吗?
- 不能,默认是浅层侦听
- 不开启deep,精确侦听对象的某个属性?
- 可以把第一个参数写成函数的写法,返回要监听的具体属性
- 作为watch函数的第一个参数,ref对象需要添加.value吗?
组合式API - 生命周期函数
Vue3的生命周期API (选项式 VS 组合式)
生命周期函数选项式对比组合式(组合式也能以选项式API的写法写)
选项式API(配置项中的函数) | 组合式API(<script setup> 中直接写函数) |
---|---|
beforeCreate/created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
左侧对应原来在vue2选项式API中写的生命周期配置项函数,在vue3中,写到右侧vue3组合式API中的函数,以实现相同功能
优先右侧组合式API写法,逻辑更方便复用、更好维护
beforeCreate 和 created 的相关代码,包括方法和调用,一律放在 setup 中执行,不需要加其他,
<script setup></script>
标签对包裹下默认就是beforeCreate/created,逻辑直接封装,直接调用
生命周期函数基本使用
- 导入生命周期函数,如
import { onMounted } from 'vue'
- 执行生命周期函数 传入回调
onMounted( () => {回调函数中的自定义逻辑} )
需要使用哪个生命周期函数,就导入哪个
onBeforeMount/onMounted
onBeforeUpdate/onUpdated
onBeforeUnmount/onUnmounted
生命周期函数导入后,
onBeforeMount/onMounted
onBeforeUpdate/onUpdated
onBeforeUnmount/onUnmounted
均以函数的形式写到<script setup></script>
标签对中销毁改为onBeforeUnmount/onUnmounted(即选项式写法的beforeUnmount/unmounted)
执行多次
生命周期函数是可以执行多次的,多次执行时传入的回调会在时机成熟时依次执行
使用时,以回调函数形式使用,在回调函数中写自定义逻辑
生命周期函数写成函数的调用方式后,可以调用多次,并不会冲突,而是按照顺序依次执行
<script setup>
import { onMounted } from 'vue';
// beforeCreate 和 created 的相关代码
// 一律放在 setup 中执行
const getList = () => {
setTimeout(() => {
console.log('发送请求,获取数据')
}, 2000)
}
// 一进入页面的请求
getList()
// 如果有些代码需要在mounted生命周期中执行
onMounted(() => {
console.log('mounted生命周期函数 - 逻辑1')
})
// 写成函数的调用方式,可以调用多次,并不会冲突,而是按照顺序依次执行
onMounted(() => {
console.log('mounted生命周期函数 - 逻辑2')
})
</script>
<template>
<div></div>
</template>
- 总结:
- 组合式API中生命周期函数的格式是什么?
- on + 生命周期名字
- 组合式API中可以使用onCreated吗?
- 没有这个钩子函数,直接写到setup中
- 组合式API中组件卸载完毕时执行哪个函数?
- onUnmounted
组合式API - 父子通信
组合式API下的父传子
- 基本思想
- 父组件中给子组件绑定属性
- 父组件中,在子组件的标签对上,写传递的数据属性名
- 子组件内部通过props选项接收
- 子组件中,通过defineProps({})函数作接收
- 父组件中给子组件绑定属性

由于改为组合式api,子组件中没有了原来的props配置对象,改为通过
defineProps({})
函数作接收接收后的数据,在模板中使用,直接 写传递的数据属性名 即可渲染
可以传递动态的/响应式的数据,通过
:传递的数据属性名="script setup中的变量名/或template中的渲染名"
defineProps 原理:就是编译阶段的一个标识,实际编译器解析时,遇到后会进行编译转换
<script setup>
// 父传子
// 1. 给子组件,添加属性的方式传值
// 2. 在子组件,通过props接收
import { ref } from 'vue'
// 以局部组件的形式引入(局部组件,导入进来就能用)
import SonCom from '@/components/son-com.vue'
const money = ref(100)
const getMoney = () => {
money.value += 10
}
</script>
<template>
<div>
<h3>
父组件 - {{ money }}
<button @click="getMoney">挣钱</button>
</h3>
<!-- 给子组件,添加属性的方式传值 -->
<SonCom car="宝马车" :money="money"></SonCom>
</div>
</template>
<script setup>
// 注意:由于写了 setup,所以无法直接配置 props 选项
// 所以:此处需要借助于 “编译器宏” 函数接收子组件传递的数据
const props = defineProps({
car: String,
money: Number
})
const emit = defineEmits(['changeMoney'])
console.log(props.car)
console.log(props.money)
const buy = () => {
// 需要 emit 触发事件
emit('changeMoney', 5)
}
</script>
<template>
<!-- 对于props传递过来的数据,模板中可以直接使用 -->
<div class="son">
我是子组件 - {{ car }} - {{ money }}
<button @click="buy">花钱</button>
</div>
</template>
<style scoped>
.son {
border: 1px solid #000;
padding: 30px;
}
</style>
总结
父传子
- 父传子的过程中通过什么方式接收props?
defineProps( { 属性名:类型 } )
- setup语法糖中如何使用父组件传过来的数据?
const props = defineProps( { 属性名:类型 } )
props.xxx
- 父传子的过程中通过什么方式接收props?
组合式API下的子传父
基本思想
- 父组件中给子组件标签通过@绑定事件
- 父组件中,在子组件的标签对上,通过
@绑定的事件名字="触发事件时,在父组件上执行的自定义逻辑的名字"
,实现对绑定的事件作侦听
- 父组件中,在子组件的标签对上,通过
- 子组件内部通过 emit 方法触发事件
- 子组件中,先通过defineEmits编译器宏,通过中括号[]数组,先声明在父组件中@绑定的事件,生成emit方法
const emit = defineEmits(['父组件中给子组件标签通过@绑定的事件名字'])
- 然后子组件中,通过生成的
emit('父组件中给子组件标签通过@绑定的事件名字', 传参)
触发事件并传参
- 子组件中,先通过defineEmits编译器宏,通过中括号[]数组,先声明在父组件中@绑定的事件,生成emit方法
- 要点:
- 子组件不能直接改父组件中的数据
- 子组件通过
const emit = defineEmits(['父组件中给子组件标签通过@绑定的事件名字'])
和emit('父组件中@绑定的事件名字',传参)
,实现事件子传父,调用事件在父组件中的逻辑,实现对父组件中的数据修改 - 通过defineEmits编译器宏自定义,将需要通知父组件调用的@绑定的事件名字,通过数组写入
- 需要触发的父组件的@绑定事件,都必须先声明了,后续才能emit触发,是vue3的规则,在子组件script中要触发的父组件中的自定义事件,必须在编译器宏中先声明
- 父组件中给子组件标签通过@绑定事件

<script setup>
// 父传子
// 1. 给子组件,添加属性的方式传值
// 2. 在子组件,通过props接收
// 子传父
// 1. 在子组件内部,emit触发事件 (编译器宏获取)
// 2. 在父组件,通过 @ 监听
// 局部组件(导入进来就能用)
import { ref } from 'vue'
import SonCom from '@/components/son-com.vue'
const money = ref(100)
const getMoney = () => {
money.value += 10
}
const changeFn = (newMoney) => {
money.value = newMoney
}
</script>
<template>
<div>
<h3>
父组件 - {{ money }}
<button @click="getMoney">挣钱</button>
</h3>
<!-- 给子组件,添加属性的方式传值 -->
<SonCom
@changeMoney="changeFn"
car="宝马车"
:money="money">
</SonCom>
</div>
</template>
<script setup>
// 注意:由于写了 setup,所以无法直接配置 props 选项
// 所以:此处需要借助于 “编译器宏” 函数接收子组件传递的数据
const props = defineProps({
car: String,
money: Number
})
// 通过defineEmits编译器宏,通过中括号[]数组,先声明在父组件中@绑定的事件
const emit = defineEmits(['changeMoney'])
console.log(props.car)
console.log(props.money)
const buy = () => {
// 需要 emit 触发事件
emit('changeMoney', 5)
}
</script>
<template>
<!-- 对于props传递过来的数据,模板中可以直接使用 -->
<div class="son">
我是子组件 - {{ car }} - {{ money }}
<button @click="buy">花钱</button>
</div>
</template>
<style scoped>
.son {
border: 1px solid #000;
padding: 30px;
}
</style>
总结
子传父
- 子传父的过程中通过什么方式得到emit方法?
defineEmits( [‘事件名称’] )
- 怎么触发事件
emit('自定义事件名', 参数)
- 子传父的过程中通过什么方式得到emit方法?
组合式API - 模版引用
模板引用的概念
- 通过ref标识获取页面中真实的dom对象或者组件实例对象
- 通过ref拿到真实的dom对象或者组件实例对象,进而调用dom中的属性或方法,调用组件中的属性或方法

模板引用(可以获取dom,也可以获取组件)
- 调用ref函数生成一个ref对象
- 通过
import { ref } from 'vue'
导入 ref 函数 - 调用ref函数
const ABCD = ref(null)
生成一个ref对象ABCD,或者说:生成一个ref对象绑定到ABCD变量
- 通过
- 通过ref标识绑定ref对象到标签
- 在
<template>
模板的标签中,通过ref="对象名ABCD"
,绑定ref对象到标签 - 后续通过
ABCD.值value
拿到对象,通过ABCD.值value.方法focus()
调用对象中的方法 - 通过
ref对象.value
即可访问到绑定的元素(但必须在渲染完成后,才能拿到)
- 在
- 个人理解:
- 通过将ref标识赋值给变量,后续在template模板标签中,通过
ref="变量"
绑定后,在script逻辑中,即可通过变量.value值
拿到对象,通过变量.value值.方法()
,实现调用对象中的方法 - 通过
变量.value值
拿到对象,包括dom对象,也可以是组件对象,再通过.
拿到对象中的.属性/方法 - 将ref标识赋值给变量,即
const ABCD = ref(null)
- 注意:通过
ref对象.value
即可访问到绑定的元素(但必须在渲染完成后,才能拿到)
- 通过将ref标识赋值给变量,后续在template模板标签中,通过
<script setup>
import TestCom from '@/components/test-com.vue'
import { onMounted, ref } from 'vue'
// 模板引用(可以获取dom,也可以获取组件)
// 1. 调用ref函数,生成一个ref对象
// 2. 通过ref标识,进行绑定
// 3. 通过ref对象.value即可访问到绑定的元素(必须渲染完成后,才能拿到)
const inp = ref(null)
// 生命周期钩子 onMounted,实现一进页面就inp.value.focus()聚焦
onMounted(() => {
// 在onMounted渲染完成后,才能console.log打印出
// console.log(inp.value)
// inp.value.focus()
})
// 点按钮聚焦
const clickFn = () => {
// 通过inp.value,拿到<input ref="inp" type="text">所在的input标签,再调用方法
inp.value.focus()
}
</script>
<template>
<div>
<input ref="inp" type="text">
<button @click="clickFn">点击让输入框聚焦</button>
<TestCom></TestCom>
</div>
</template>
<script setup>
import TestCom from '@/components/test-com.vue'
import { onMounted, ref } from 'vue'
// 模板引用(可以获取dom,也可以获取组件)
// 1. 调用ref函数,生成一个ref对象
// 2. 通过ref标识,进行绑定
// 3. 通过ref对象.value即可访问到绑定的元素(必须渲染完成后,才能拿到)
const inp = ref(null)
// 生命周期钩子 onMounted,实现一进页面就inp.value.focus()聚焦
onMounted(() => {
// 在onMounted渲染完成后,才能console.log打印出
// console.log(inp.value)
// inp.value.focus()
})
// 点按钮聚焦
const clickFn = () => {
// 通过inp.value,拿到<input ref="inp" type="text">所在的input标签,再调用方法
inp.value.focus()
}
// ------------------以下为操作组件--------------------
const testRef = ref(null)
const getCom = () => {
console.log(testRef.value.count)
// 通过testRef.value,拿到<TestCom ref="testRef"></TestCom>所在的TestCom组件,再调用方法
testRef.value.sayHi()
}
</script>
<template>
<div>
<input ref="inp" type="text">
<button @click="clickFn">点击让输入框聚焦</button>
</div>
<TestCom ref="testRef"></TestCom>
<button @click="getCom">获取组件</button>
</template>
子组件的属性如果需要被外部获取,需要通过defineExpose()导出属性
- vue3中,默认情况下在
<script setup>
语法糖下组件内部的属性和方法是不开放给父组件/外部访问的 - 可以通过defineExpose编译宏函数,指定本组件中的哪些属性和方法,允许外部访问
<script setup>
const count = 999
const sayHi = () => {
console.log('打招呼')
}
// 允许暴露本组件的方法/数据
defineExpose({
count,
sayHi
})
</script>
<template>
<div>
我是用于测试的组件 - {{ count }}
</div>
</template>
- 总结:
- 获取模板引用的时机是什么?
- 组件挂载完毕
- defineExpose编译宏的作用是什么?
- 显式暴露组件内部的属性和方法
组合式API - provide和inject
- provide和inject是vue3中使用非常广泛的语法,可以实现跨层级共享数据,使用度非常高
作用和场景
- 从顶层组件,向任意的底层组件,传递数据和方法,实现跨层组件通信
跨层传递普通数据\响应式数据\传递方法
顶层组件通过provide函数提供数据
- 跨层传递普通数据
provide('数据关键字key',顶层组件中的数据)
- 跨层传递响应式数据
provide('变量关键字key',顶层组件中的响应式数据ref对象/带方法的变量)
- 顶层组件可以向底层组件传递方法
provide('方法关键字key',顶层组件中的箭头函数 (接收的传参) => {方法的具体逻辑} )
- 跨层传递普通数据
底层组件通过inject函数获取数据/底层组件通过调用方法修改顶层组件中的数据
inject('数据/变量关键字key')
,可通过赋值变量接收数据/变量中的数据,如const ABCD = inject('数据/变量关键字key')
,后续在模板中可直接使用变量名inject('方法关键字key')
,可通过赋值变量接收方法,如const EFGH = inject('方法关键字key')
,后续可通过EFGH(传参)
发起调用
- 备注:
- 写数据相关的逻辑时,应遵循原则,谁的数据谁来维护
- 例如数据是父组件的,那么修改数据的方法逻辑,应该写在父组件的逻辑中
<script setup>
// 在顶层组件中,导入provide函数
import { provide, ref } from 'vue'
// 引入中间层组件
import CenterCom from '@/components/center-com.vue'
// 1. 跨层传递普通数据
provide('theme-color', 'pink')
// 2. 跨层传递响应式数据
const count = ref(100)
provide('count', count)
// 两秒后修改数据为500
setTimeout(() => {
count.value = 500
}, 2000)
// 3. 跨层传递函数 => 给子孙后代传递可以修改数据的方法
provide('changeCount', (newCount) => {
count.value = newCount
})
</script>
<template>
<div>
<h1>我是顶层组件</h1>
<CenterCom></CenterCom>
</div>
</template>
<script setup>
// 引入底层组件
import BottomCom from './bottom-com.vue'
</script>
<template>
<div>
<h2>我是中间组件</h2>
<BottomCom></BottomCom>
</div>
</template>
<script setup>
// 在底层组件中引入inject函数
import { inject } from 'vue'
// 通过顶层组件的数据关键字key,通过inject('数据关键字key')接收
const themeColor = inject('theme-color')
// 可以接收响应式数据
const count = inject('count')
// 可以接收方法
const changeCount = inject('changeCount')
const clickFn = () => {
// 接收方法后,发起传参调用
changeCount(1000)
}
</script>
<template>
<div>
<h3>我是底层组件-{{ themeColor }} - {{ count }}</h3>
<button @click="clickFn">更新count</button>
</div>
</template>
需求解决思考
- provide和inject的作用是什么?
- 跨层组件通信
- 如何在传递的过程中保持数据响应式?
- 第二个参数传递ref对象
- 底层组件想要通知顶层组件做修改,如何做?
- 传递方法,底层组件调用方法
- 一颗组件树中只有一个顶层或底层组件吗?
- 相对概念,存在多个顶层和底层的关系
Vue3.3新特性
Vue3.3新特性-defineOptions
- 背景说明:
- 有
<script setup>
之前,如果要定义 props, emits 可以轻而易举地添加一个与 setup 平级的属性。 - 但是用了
<script setup>
后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。
- 有

为了解决这一问题,引入了 defineProps 与 defineEmits 这两个宏,但这只解决了 props 与 emits 这两个属性。
- 如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的
<script>
标签。这样就会存在两个<script>
标签。让人无法接受。
- 如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的
所以在 Vue 3.3 中新引入了 defineOptions 宏。
- 顾名思义,主要是用来定义 Options API 的选项。
- 可以用defineOptions 定义任意的选项, props, emits, expose, slots 除外(因为这些可以使用 defineXXX 来做到)
个人理解:
- 原来在script中,与setup平级的属性,改为放到defineOptions({函数中定义各个平级属性})
defineOptions语法
<script setup>
defineOptions({
name: '名字XXindex',
任意选项: true,
// ...等等更多其他自定义属性
})
</script>
Vue3.3新特性-defineModel
新特性意义:为了简化v-model,快速实现双向绑定
Vue3 中的 v-model 和 defineModel对比:
在Vue3中,自定义组件上使用v-model, 相当于传递一个modelValue属性,同时触发 update:modelValue 事件
我们需要先定义 props,再定义 emits,再去@触发事件。其中有许多重复的代码。如果需要修改此值,还需要手动调用 emit 函数。
Vue3 中的 v-model 实现案例
<script setup>
import MyInput from '@/components/my-input.vue'
import { ref } from 'vue'
const txt = ref('123456')
</script>
<template>
<div>
<MyInput v-model="txt"></MyInput>
{{ txt }}
</div>
</template>
<script setup>
// 接收
defineProps({
modelValue: String
})
// 定义子传父emit方法
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<div>
<!-- 监听事件,将最新输入框的值e.target.value事件对象,传参调用方法 -->
<input
type="text"
:value="modelValue"
@input="e => emit('update:modelValue', e.target.value)"
>
</div>
</template>
<script setup>
// 引入 defineModel方法
import { defineModel } from 'vue'
// 接收 defineModel方法
const modelValue = defineModel()
</script>
<template>
<div>
<!-- 通过 :value="赋值给modelValue方法"-->
<!-- 通过将e.target.value传参给modelValue方法 -->
<input
type="text"
:value="modelValue"
@input="e => modelValue = e.target.value"
>
</div>
</template>
- 注意,需要到vite.config.js中,先开启defineModel
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
script: {
// 开启defineModel,且在开启后需要重新pnpm serve
defineModel: true
}
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
Pinia 快速入门
什么是Pinia
Pinia 是 Vue专属的 最新状态管理库/工具 ,是 Vuex 状态管理工具的替代品
Pinia特点:
- 提供更加简单的API(去掉了 mutation)
- 提供符合 组合式风格的API (和 Vue3 新语法统一)
- 即同一个逻辑的声明数据、actions提供修改数据的方法、数据相关的计算属性、getter等,组合到一个api
- 去掉了 modules 的概念,每一个 store 都是一个独立的模块
- 可以创建多个独立的 store 仓库整体,也支持跨模块调用数据,不需要开namespace
- 配合 Typescript 更加友好,提供可靠的类型推断
两个vue状态管理库/工具的对比(pinia同时支持 Vue 2 和 Vue 3,但在vue2中不能与 Vuex 一起使用)
vuex | pinia |
---|---|
state | state |
mutations | / |
actions | actions |
getters | getters |
modules | / |
手动添加Pinia到Vue项目
在实际开发项目的时候,关于Pinia的配置,可以在项目创建时自动添加,现在我们初次学习,从零开始:
- 使用 Vite 创建一个空的 Vue3 项目
npm create vue@latest
或pnpm create vue@latest
- 按照官方文档 安装 pinia 到项目中
yarn add pinia
或npm install pinia
或pnpm install pinia
- vue3:创建一个 pinia 实例 (根 store) 并将其传递给应用(到main.js,完成引入安装)
- 使用 Vite 创建一个空的 Vue3 项目
在vue3项目中导入pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建Pinia实例
const pinia = createPinia()
// 挂载到app上,建立关联,支持链式
createApp(App).use(pinia).mount('#app')
// 可以将前半部分提取到变量,以创建根实例
const app = createApp(App) // 创建根实例
app.use(pinia).mount('#app') // 挂载
// app.use(pinia).mount('#app')可以分为两行写
app.use(pinia) // pinia插件的安装配置
app.mount('#app') // 视图的挂载
Pinia基础使用 - 计数器案例 定义store
- 定义store
- 新建一个store模块,在模块中维护数据;导入包
import { defineStore } from 'pinia'
,并导出模块export const use第一个参数声明仓库的唯一标识Store = defineStore('第一个参数声明仓库的唯一标识', { 第二个参数/其他配置...})
- 由于Pinia中不同的仓库模块是相互独立的,因此每个仓库模块,都需要有仓库的唯一标识作为区分,写到第一个参数上
- 可以在src/store目录下,任意新建数量任意的子仓库.js,且每一个仓库是独立的
- 最终所有的store仓库模块,都会挂载到同一个状态树上
- 第二个参数可以自行添加配置项,可以是组合式api(传入箭头函数),也可以是选项式api(配置项)
- https://pinia.vuejs.org/zh/core-concepts/
- 新建一个store模块,在模块中维护数据;导入包
重点:
- 在Setup Store,即由组合式api中定义的store
- ref() 就是 state 属性,声明数据
- computed() 就是 getters,声明基于数据派生的计算属性 getters (computed)
- function() 就是 actions,声明操作数据的方法 action (普通函数)
- 页面中使用定义的store数据,需要在箭头函数中return(Setup函数中return),后续页面中才能使用
- 定义好的store数据,通过const变量,以函数的形式接收,通过export导出,即
export const use仓库名store = defineStore('第一个参数声明仓库的唯一标识', 第二个参数()=>{箭头函数return数据导出})
- 为了养成习惯性的用法,将返回的函数命名为 use... 是一个符合组合式函数风格的约定,前面是use,后面是store,中间是仓库名,即
use仓库名store
- 后续通过调用函数,就能拿到return暴露的数据
- 在Setup Store,即由组合式api中定义的store
备注:
- store相关命名规则的简单理解:
- 作为函数/方法作导出时,导出的名字前加上use,名字后加上store
- 在页面中导入使用时,需要导入函数/方法
import{ use函数/方法store }
- 导入后,用 const 函数/方法的名字 建立变量,调用
use函数/方法store()
实现赋值
- 方法命名规则的简单理解:
- 通过方法的命名,体现所属模块和功能,如模块名use模块下的方法名Register注册相关后缀Service服务
- 模块名+方法名+后缀
- store相关命名规则的简单理解:
选项式定义及使用store
import { defineStore } from 'pinia'
// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
- 组合式api定义及使用store(即Setup Store,与 Vue组合式API 的 setup函数 相似)
import { defineStore } from 'pinia'
// 定义store,利用defineStore方法去定义
// defineStore('第一个参数仓库的唯一标识,通常写仓库名', 第二个参数箭头函数() => { ... })
// 在第二个参数中,通过传入箭头函数,以组合式api使用
export const useCounterStore = defineStore('counter', () => {
// ref()定义响应式数据
const count = ref(0)
// 定义数据的派生状态
const doubleCount = computed(() => count.value * 2)
// 定义方法,通过提供function方法,就能修改数据
function increment() {
count.value++
}
// 将数据和方法对外暴露,在页面中就可以使用
return { count, doubleCount, increment }
})
// 备注
ref() 就是 state 属性
computed() 就是 getters
function() 就是 actions
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
// 定义store,利用defineStore方法去定义
// defineStore('第一个参数仓库的唯一标识,通常写仓库名', 第二个参数箭头函数() => { ... })
export const useCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(100)
// 声明操作数据的方法 action (普通函数)
const addCount = () => count.value++
const subCount = () => count.value--
// 声明基于数据派生的计算属性 getters (computed)
const double = computed(() => count.value * 2)
// 声明数据 state - msg
const msg = ref('hello pinia')
return {
count,
double,
addCount,
subCount,
msg,
}
}, {
// persist: true // 开启当前模块的持久化
persist: {
key: 'hm-counter', // 修改本地存储的唯一标识
paths: ['count'] // 存储的是哪些数据
}
})
Pinia基础使用 - 计数器案例 组件使用store
- 组件使用store
- 页面中,通过
import { use仓库名store } from '@/store/定义的store函数文件名'
通过引入函数来引入store仓库 - 通过export导出暴露的仓库实例,基于
use仓库名store
方法, - 通过
const ABCDStore = use仓库名store()
调用方法,得到当前仓库实例,使用store数据,此时ABCDStore
是一个对象 - 如此时,对
ABCDStore
直接解构,不处理,数据会丢失响应式,如const { 解构出的store键名key } = storeToRefs(ABCDStore)
- 页面中,通过
- 备注:
- 你可以定义任意多的 store,但为了让使用 pinia 的益处最大化 (比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store
- 虽然我们前面定义了一个 store,但在我们使用
<script setup>
调用useStore()(或者使用 setup() 函数,像所有的组件那样)
之前,store 实例是不会被创建的,需要被使用了,store才会被创建 - 可以理解为异步store,需要用到请求到哪个仓库,store才会被异步建立
<script setup>
import { storeToRefs } from 'pinia'
import Son1Com from '@/components/Son1Com.vue'
import Son2Com from '@/components/Son2Com.vue'
import { useCounterStore } from '@/store/counter'
import { useChannelStore } from './store/channel'
const counterStore = useCounterStore()
const channelStore = useChannelStore()
</script>
<template>
<div>
<!-- 直接使用return对外暴露的数据 -->
<h3>
App.vue根组件
- {{ count }}
- {{ msg }}
</h3>
<Son1Com></Son1Com>
<Son2Com></Son2Com>
<hr>
<button @click="getList">获取频道数据</button>
<ul>
<li v-for="item in channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<style scoped>
</style>
<script setup>
import { useCounterStore } from '@/store/counter'
const counterStore = useCounterStore()
</script>
<template>
<div>
我是Son1.vue - {{ counterStore.count }} - {{ counterStore.double }}
<button @click="counterStore.addCount">+</button>
</div>
</template>
<style scoped>
</style>
<script setup>
import { useCounterStore } from '@/store/counter'
const counterStore = useCounterStore()
</script>
<template>
<div>
我是Son1.vue - {{ counterStore.count }} - {{ counterStore.double }}
<button @click="counterStore.addCount">+</button>
</div>
</template>
<style scoped>
</style>
getters实现
Pinia中的 getters ,是直接利用 computed函数 进行模拟
组件中需要使用需要把 getters return出去
直接使用computed函数作getters模拟的语法
// computed函数作getters模拟的语法
export const use定义的store函数名 = defineStore('仓库的唯一标识', () => {
const 一个派生属性 = computed(() => 传一个数据派生方法)
// 对外暴露
return {
一个派生属性
}
}
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
// 定义store,利用defineStore方法去定义
// defineStore('第一个参数仓库的唯一标识,通常写仓库名', 第二个参数箭头函数() => { ... })
export const useCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(100)
// 声明操作数据的方法 action (普通函数)
const addCount = () => count.value++
const subCount = () => count.value--
// 声明基于数据派生的计算属性 getters (computed)
const double = computed(() => count.value * 2)
// 声明数据 state - msg
const msg = ref('hello pinia')
return {
count,
double,
addCount,
subCount,
msg,
}
}, {
// persist: true // 开启当前模块的持久化
persist: {
key: 'hm-counter', // 修改本地存储的唯一标识
paths: ['count'] // 存储的是哪些数据
}
})
action异步实现
Pinia中的action,既支持同步,也支持异步
- 函数中既可以action修改数据,也能先发请求,拿到返回后的数据,再去修改
编写方式:异步action函数的写法,和组件中获取异步数据的写法完全一致
- 添加异步逻辑:在箭头函数中,正常定义操作数据的方法,直接在操作数据的方法中,写入异步请求
- 可以继续往下封装方法,导进来直接调用
需求:在Pinia中获取频道列表数据并把数据渲染App组件的模板中
- 接口地址:http://geek.itheima.net/v1_0/channels
- 数据结构如下图

import axios from 'axios'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useChannelStore = defineStore('channel', () => {
// 声明数据,发请求后返回的数据往数组中存放
const channelList = ref([])
// 声明操作数据的方法,async写在箭头函数返回值前
const getList = async () => {
// 支持异步,直接解构
const { data: { data } } = await axios.get('http://geek.itheima.net/v1_0/channels')
// 解构后赋值回去,向外导出
channelList.value = data.channels
}
// 声明getters相关
return {
channelList,
getList
}
})
<script setup>
import Son1Com from '@/components/Son1Com.vue'
import Son2Com from '@/components/Son2Com.vue'
import { useCounterStore } from '@/store/counter'
import { storeToRefs } from 'pinia'
import { useChannelStore } from './store/channel'
const counterStore = useCounterStore()
const channelStore = useChannelStore()
</script>
<template>
<div>
<!-- 直接使用return对外暴露的数据 -->
<h3>
App.vue根组件
- {{ count }}
- {{ msg }}
</h3>
<Son1Com></Son1Com>
<Son2Com></Son2Com>
<hr>
<button @click="channelStore.getList">获取频道数据</button>
<ul>
<li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<style scoped>
</style>
storeToRefs工具函数
使用storeToRefs辅助函数,可以保持数据(state + getter)的响应式解构
- 可通过
import { storeToRefs } from 'pinia'
从pinia导入storeToRefs工具函数 - 通过传入
const { 解构后数据名 } = storeToRefs(解构对象)
方法,响应式解构,简化模板数据
- 可通过
注意:
- 请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。
- 就像 setup 中的 props 一样,我们不能对它进行解构,因为解构相当于直接声明了变量去接收
// 直接解构,不处理,数据会丢失响应式
const { count, msg } = counterStore
// 通过storeToRefs辅助函数解构,数据保持响应式
const { count, msg } = storeToRefs(counterStore)
// 方法并不需要storeToRefs处理,直接解构,解构后直接使用
const { getList } = channelStore
<script setup>
import Son1Com from '@/components/Son1Com.vue'
import Son2Com from '@/components/Son2Com.vue'
import { useCounterStore } from '@/store/counter'
import { storeToRefs } from 'pinia'
import { useChannelStore } from './store/channel'
const counterStore = useCounterStore()
const channelStore = useChannelStore()
// 直接解构,不处理,数据会丢失响应式
// const { count, msg } = counterStore
// 通过storeToRefs辅助函数解构,数据保持响应式
const { count, msg } = storeToRefs(counterStore)
const { channelList } = storeToRefs(channelStore)
// 方法并不需要storeToRefs处理,直接解构,解构后直接使用
const { getList } = channelStore
</script>
<template>
<div>
<!-- 直接使用return对外暴露的数据 -->
<h3>
App.vue根组件
- {{ count }}
- {{ msg }}
</h3>
<Son1Com></Son1Com>
<Son2Com></Son2Com>
<hr>
<button @click="getList">获取频道数据</button>
<ul>
<li v-for="item in channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<style scoped>
</style>
Pinia的调试
- Vue官方的 dev-tools 调试工具 对 Pinia直接支持,可以直接进行调试
- 类似vuex的调试

Pinia持久化存储插件
背景:
- vuex中,通过
localStorage.setItem(数据名)
、localStorage.getItem(数据名)
、localStorage.removeItem(数据名)
封装,实现vuex持久化/本地存储的设置/获取/清空处理 - vuex中,也可使用vuex-persistedstate实现持久化
- vuex中,通过
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
步骤:
- 安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
或pnpm i pinia-plugin-persistedstate
- main.js 引入使用
- 导入持久化的插件
import persist from 'pinia-plugin-persistedstate'
app.use(createPinia().use(persist))
- 导入持久化的插件
- store仓库中,persist: true 开启
- 往本地存入时,基于仓库的唯一标识名字作存储
defineStore('第一个参数声明仓库的唯一标识'
- 在Pinia的调试工具piniadev-tools中,以及调试中的application中的storage中都能看到,key就是仓库的唯一标识
- 往本地存入时,基于仓库的唯一标识名字作存储
- pinia-plugin-persistedstate插件的默认配置如下
- localStorage 作为存储。
- store.$id 作为存储的默认 key。
- JSON.stringify/destr 作为序列化器/反序列化器。
- 整个state状态,默认将被持久化到存储中。
- 可以向存储的persist属性,传递一个对象,来自定义配置持久性
- 传递对象以自定义配置持久化
- persist对象中,配置
key: '名字ABCD'
,修改本地存储的唯一标识为名字ABCD - persist对象中,配置
paths: ['数据key键名']
,以选择性地保留存储键名key的数据 - 新版本下,paths配置改为pick
- 安装插件 pinia-plugin-persistedstate
Pinia持久化存储插件的引入
- pinia-plugin-persistedstate引入&安装语法,将插件添加到 pinia 实例中
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
import App from './App.vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入持久化的插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 可改为
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia() // 创建Pinia实例
const app = createApp(App) // 创建根实例
// 插件添加到pinia,再把pinia添加到app中
app.use(pinia.use(piniaPluginPersistedstate)) // pinia插件的安装配置
// 可改为
app.use(pinia.use(persist)) // pinia插件的安装配置
app.mount('#app') // 视图的挂载
Pinia持久化存储插件的使用
用法:在声明store时,将persist选项设置为 true,即
persist: true
选项式api用法,在与state、actions、getter并列位置上,写
persist: true
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useStore = defineStore(
'main',
() => {
const someState = ref('hello pinia')
return { someState }
},
{
persist: true,
},
)
- 组合式api用法,在defineStore中的第三个参数的位置,即回调函数后的位置,写
persist: true
export const use仓库名store = defineStore('第一个参数声明仓库的唯一标识', 第二个参数()=>{箭头函数return数据导出},persist: true)
- 或
export const use第一个参数声明仓库的唯一标识Store = defineStore('第一个参数声明仓库的唯一标识', { 第二个参数/其他配置...,persist: true})
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
someState: 'hello pinia',
}
},
persist: true,
})
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
// 定义store,利用defineStore方法去定义
// defineStore('第一个参数仓库的唯一标识', 第二个参数箭头函数() => { ... })
export const useCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(100)
// 声明操作数据的方法 action (普通函数)
const addCount = () => count.value++
const subCount = () => count.value--
// 声明基于数据派生的计算属性 getters (computed)
const double = computed(() => count.value * 2)
// 声明数据 state - msg
const msg = ref('hello pinia')
return {
count,
double,
addCount,
subCount,
msg,
}
},
// 第三个参数
{
// 开启当前模块的持久化
// persist: true
// 传递对象以自定义配置持久化
persist: {
key: 'hm-counter', // 修改本地存储的唯一标识
paths: ['count'] // 存储的是哪些数据
}
}
)
- 总结:
- Pinia是用来做什么的?
- 新一代的状态管理工具,替代vuex
- Pinia中还需要mutation吗?
- 不需要,action 既支持同步也支持异步
- Pinia如何实现getter?
- computed计算属性函数
- Pinia产生的Store如何解构赋值数据保持响应式?
- 通过storeToRefs()辅助函数包裹
- Pinia 如何快速实现持久化?
- pinia-plugin-persistedstate