学习笔记
1 . Vue3中.vue文件的结构
vue文件的结构
<template>
</template>
<script>
export default {
setup() {
return {}
}
}
</script>
<style lang="scss" scoped>
</style>
2. 响应式数据
2.1 响应式数据的定义ref()
、reactive()
- 变量在插值语法中使用:
- 引入
reactive
,ref
- 定义变量:变量是基本数据类型使用
ref()
,变量是对象时使用reactive()
, - 特别注意:响应式数据是基本数据类型,无论是修改还是读取变量的值,都必须加
.value
- 要在插值语法中使用的变量,必须在
return
中加入,相当于把变量暴露出去
- 引入
<script>
import {reactive, ref} from "vue";
export default {
setup() {
let a = ref(1)
a.value = 2
let b = reactive({name:'jack',age:20})
b.name = 'Peter'
return {a,b}
}
}
</script>
- 变量不在插值语法中使用,则正常定义即可,不需要做任何处理
2.2 shallowRef
、 shallowReactive
:浅层次响应
shallowRef
- 变量的值是基本数据类型,则
shallowRef
和ref
的作用是一样的,都具有响应式 - 变量的值是对象的情况
- 变量的值是基本数据类型,则
let count = shallowRef({ // 只有在更换整个count的值时才触发响应式
count1:0, //不具有响应式
a : {
count2 : 0 // 不具有响应式
}
})
shallowReactive
使用shallowReactiv
定义变量后,只有对象的最外层属性具有响应式
let count = shallowReactive({
count1:0, //最外层的属性是响应式
a : {
count2 : 0 // 不具有响应式
}
})
- 完整的代码
<template>
<div>
<div>1号计数器:{{count.count1}}</div>
<-- 点击下面的按钮,计数器可以运行 -->
<button @click="count.count1++">+1</button>
<hr>
<div>2号计数器:{{count.a.count2}}</div>
<-- 点击下面的按钮,计数器不运行 -->
<button @click="count.a.count2++">+1</button>
</div>
</template>
<script>
import {reactive, shallowReactive} from "vue";
export default {
name : 'app',
setup(){
let count = shallowReactive({
count1:0,
a : {
count2 : 0
}
})
return {count}
}
}
</script>
2.3 shallowReadonly
:浅只读、readonly
:深只读
let count = reactive({
count1:0, // shallowReadonly:这层数据不可修改,里面的子对象可以修改
a : {
count2 : 0 // shallowReadonly:可以修改
}
})
// 深只读:数据count中所有属性都不可修改
count = readonly(count)
// 浅只读:数据count中最完成的属性不可修改,里面的子对象的属性可以修改
count= shallowReadonly(count)
2.4 判断变量的响应式类型
isRef()
:检查一个对象是否为ref()
创建的代理。
isReactive()
:检查一个对象是否是由reactive()
或shallowReactive()
创建的代理。
isProxy()
:检查一个对象是否是由reactive()
、readonly()
、shallowReactive()
或shallowReadonly()
创建的代理。
isReadonly()
:检查传入的值是否为只读对象readonly()
。
2.5 toRef 和 toRefs
// 原来的写法
<div>{{count.count1}}</div>
<div>{{count.a.count2}}</div>
let count = reactive({
count1:0,
a : {
count2 : 0
}
})
return {count}
- toRef
- 作用:给对象中比较深层的属性起个别名,方便在插值语法中使用
- 使用:
return {
count1 : toRef(count,'count1'),
count2 : toRef(count.a,'count2')
}
// 使用插值语法时
<div>{{count1}}</div>
<div>{{count2}}</div>
- toRefs
- 作用:和toRef相同,使用时会自动生成别名
- 缺点:只能省略最外层
return {
...toRefs(count)
}
// 在插值语法中使用时
<div>{{count1}}</div>
<div>{{a.count2}}</div>
2.6 toRaw:响应式数据转换为原始数据
- 只能用于
reactive
定义的变量 toRaw返回的变量
在发生改变时不会引起页面重新渲染
let data = reactive({
count: 0
})
// 去除响应式,data与dataToRaw共用一个对象,两个常量指向的是同一个地址
// dataToRaw中属性有变化不会引起页面的重新渲染
let dataToRaw = toRaw(data)
// 无论是data.count++时,还是dataToRaw.count++,对象中的count1属性都会加1
data.count++
dataToRaw.count++
// 区别:
//data.count++ 时,页面会重新渲染,插值{{data.count}}和{{dataToRaw.count}}会发生变化;
// dataToRaw.count++ 时,对象属性count也会++,但是所有的插值都不会发生变化
- 完整代码
<template>
<div>
<div>data.count:{{ data.count }}</div>
<div>dataToRaw.count:{{ dataToRaw.count }}</div>
<hr>
<button @click="dataButton">响应式+1</button>
<button @click="toRawButton">去除响应+1</button>
</div>
</template>
<script>
import {reactive, toRaw} from "vue";
export default {
name: 'app',
setup() {
let data = reactive({
count: 0
})
let dataToRaw = toRaw(data)
function toRawButton() {
dataToRaw.count++
console.log('data:',data.count,'dataToRaw:',dataToRaw.count)
}
function dataButton(){
data.count++
console.log('data:',data.count,'dataToRaw:',dataToRaw.count)
}
return {data,dataToRaw, toRawButton,dataButton}
}
}
</script>
2.7 markRaw:标记为原始数据
- 作用:给现有的reactive对象,添加新的属性时使用markRaw包裹,则新添加的属性不具有响应式
- 代码瞎写的,可能有误
let data = reactive({
a : 1
})
data.b = markRaw({
c : 0 //不具有响应式
})
3. 函数:function(){}
- 在
setup
中使用function
定义函数 - 加入
return
export default {
setup() {
function a(){
let b = 1
return b+1
}
return {a}
}
}
4. computed:计算属性
- 在
setup
函数中使用computed
- 不使用
set
方法时的简写形式,computed
使用箭头函数 computed
返回的变量自动成为响应式数据,不需要使用ref函数
声明
<template>
<div>
<input type="text" v-model="beforeString" />
<input type="text" v-model="afterString" />
</div>
</template>
<script>
import {computed, ref} from "vue";
export default {
name : 'app',
setup(){
let beforeString = ref('')
let afterString = ''
// 计算属性:没有set方法,使用简写形式,必须使用箭头函数
// afterString = computed(()=>{
// return beforeString.value.split('').reverse().join('')
// })
// 计算属性:set和get方法
afterString = computed({
get(){
return beforeString.value.split('').reverse().join('')
},
set(val) {
beforeString.value = val.split('').reverse().join('')
}
})
return {beforeString,afterString}
}
}
</script>
5. watch: 监听属性
watch(被监听的对象,(新值,旧值)=>{
// 被监听对象发生变化后,需要执行的逻辑代码
},{配置项})
5.1 监听基本数据类型ref()
示例的html部分
<h1>0号计数器:{{counter0}}</h1><br>
<button @click="counter0 ++">0号计数器加1</button>
<br>
<h1>1号计数器:{{counter1}}</h1><br>
<button @click="counter1 ++">1号计数器加1</button>
示例的js部分
- 监听的基本写法,配置项
immediate
在第一次加载时是否执行
watch(counter1,(newvalue,oldvalue)=>{
console.log(newvalue+' '+oldvalue)
},{immediate:true})
- 以数组形式监听,返回的newvalue和oldvalue也是数组形式
watch([counter0,counter1],(newvalue,oldvalue)=>{
console.log(newvalue+' '+oldvalue)
})
结果:
5.2 监听的是对象reactive()
- 无法检测
oldvalue
,它的值永远是newvalue
- 配置项深度监听:
deep:boolean
,没有用,无论配置成什么都是开启深度监听
被监听的对象模型
let counter2 = reactive({
firstCount : 0,
a : {
b : {
c : {
secondCount : 0,
thirdCount : 0
}
}
}
})
- 监听整个
reactive()
对象
对象中的数据有任何变化都会触发监听事件
watch(counter2,(newvalue,oldvalue)=>{
console.log(newvalue+' '+oldvalue)
},{immediate:true})
- 监听对象中的属性,这个属性是个基本数据类型,newvalue和oldvalue都有值
watch(()=>counter2.firstCount,(newvalue,oldvalue)=>{
console.log(newvalue+' '+oldvalue)
},{immediate:true})
- 监听对象中的属性,这个属性也是一个对象,这种情况等同于第一种情况
watch(counter2.a.b.c,(newvalue,oldvalue)=>{
console.log(newvalue+' '+oldvalue)
},{immediate:true})
5.3 监听对象来自store
- 代码功能:点击刷新按钮,
store
中的refresh
的值发生变化,在需要刷新页面的地方监听该变量,当变量发生改变时,刷新页面 - 代码中的监听对象
layoutSettingStore.refresh
,来自于store
中的变量需要使用箭头函数包裹
//
<script setup lang='ts'>
import useLayoutSettingStore from '@/store/modules/setting.ts'
const layoutSettingStore = useLayoutSettingStore()
import { watch } from 'vue'
watch(
() => {
return layoutSettingStore.refresh
},
() => {
// 组件中使用v-if='isRefresh',isRefresh的值为false时销毁该组件
isRefresh.value = false
// nextTick:浏览器更新完DOM时。
// 在本示例中nextTick是当组件销毁完成时,重新创建该组件,以达到刷新的效果
nextTick(() => {
isRefresh.value = true
})
},
)
</script>
// @/store/modules/setting.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
const useLayoutSettingStore = defineStore('layoutBreadcrumbStore', () => {
const refresh = ref(false) // 头部的刷新按钮,点击后,用于控制刷新
return {
refresh,
}
})
export default useLayoutSettingStore
6. watchEffect:
watchEffect(()=>{
// todo
// 在代码中出现的响应式数据发生变化,此代码就会执行
})
- watchEffect自动收集依赖数据,依赖数据更新时重新执行自身
- 立即执行,没有惰性,页面的首次加载就会执行
- 无法获取到原值,只能得到变化后的值
- 不用指明监视哪个属性,监视的回调中用到哪个属性就监视哪个属性
- 注重执行代码的逻辑过程
<template>
<input type="number" v-model="addend1"><br>
<input type="number" v-model="addend2"><br>
<div> 两数的和:{{sum}}</div>
</template>
<script>
import {ref, watchEffect} from "vue";
export default {
name : 'app',
setup(){
let addend1 = ref()
let addend2 = ref()
let sum = ref()
watchEffect(()=>{
sum.value = addend1.value + addend2.value
})
return {addend1,addend2,sum}
}
}
</script>
7. 钩子函数
- 与Vue2 中的mixin相同,达到代码复用的功能
- 新建目录,新建文件,写入钩子函数
export const 函数名 = function(){
//todo}
- 在使用函数的地方导入
import {钩子函数的函数名} from ‘钩子函数所在的文件’
8. 组件的生命周期
- 在Vue3中一般不使用
beforeCreate
和created
两个生命周期的钩子函数,因为在这两个生命周期前有setup
函数要执行,所以setup
代替了上述两个函数 - 其他生命周期函数,比Vue2多加了个on
onBeforeMount() // 组件加载前
onMounted() // 组件加载后
onBeforeUpdate() // 在组件即将因为响应式状态变更而更新前
onUpdated() // 在组件即将因为响应式状态变更而更新后
onBeforeUnmount() // 在组件实例被卸载之前调用
onUnmounted() // 在组件实例被卸载之后调用
8.1 生命周期函数的使用
- 生命周期函数被调用前,需要引入,
- 只能使用箭头函数
- 需要写在
setup
函数中 - 也可以使用Vue2中方式,但是要写在
setup
函数的外边
import {onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from "vue";
export default {
setup() {
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
}
}
9. props:父向子传递数据
9.1 非setup语法糖写法
- 发送数据和Vue2中一样,只需要在标签内,
v-bind:自定义的属性名
就可以传递
<template>
<User :name="name" :age="age"></User>
</template>
<script>
import {ref} from "vue";
import User from './components/User.vue'
export default {
name : 'app',
components : {User},
setup(){
let name = ref('jack')
let age = ref(20)
return {name,age}
}
}
</script>
- 接收数据后可直接使用
<template>
<div>{{name}}</div>
<div>{{age}}</div>
</template>
<script>
export default {
props:['name','age'],
setup() {
return { }
}
}
</script>
9.2 defineProps : setup语法糖使用
- 父组件向子组件传递a,b两个数据
父组件
<template>
<user a="非响应数据" :b="ShuJu"></user>
</template>
<script setup>
import user from '@/components/user.vue'
import {ref} from "vue";
const ShuJu=ref('响应式数据')
</script>
- 子组件有两种写法
- 直接使用
子组件`user.vue`
<template>
a:{{a}}
<br>
b:{{b}}
</template>
<script setup>
defineProps(['a','b'])
</script>
-
- 间接使用
子组件`user.vue`
<template>
a:{{c.a}}
<br>
b:{{c.b}}
</template>
<script setup>
const c = defineProps(['a','b'])
</script>
- 结果
10. defineExpose : 父、子互传
10.1 ref :子传父
- 父组件通过标签属性
ref
获得子组件暴露的数据
- 代码好像有点多,其实逻辑很简单
- 子组件中定义了一个响应式数据count,一个让count加1的方法addOne,然后暴露出去
子组件defineExpose_test.vue
<template>
计数器:{{count}}
</template>
<script setup>
import {ref} from "vue";
const count = ref(0)
function addOne() {
count.value++
}
// 暴露给父组件,一个方法,一个响应式数据
defineExpose({
count,
addOne
})
</script>
- 父组件这边比较难说明,看图
父组件
<template>
<defineExpose_test ref="sonData"></defineExpose_test><br>
<button @click="abc">控制儿子的计数器加1</button><br>
<button @click="qingLing">控制儿子的计数器清零</button>
</template>
<script setup>
// 导入子组件
import defineExpose_test from '@/components/defineExpose_test.vue'
import {ref} from "vue";
// 不会说了,看图
const sonData = ref()
// 使用儿子的数据时,记得加.value
function abc(){
// 调用儿子暴露的方法
sonData.value.addOne()
}
function qingLing(){
// 调用儿子暴露的响应式数据
sonData.value.count = 0
}
</script>
3. 补充如果使用插值语法,就不需要使用.value
10.2 $parent : 父传子
- 子组件通过属性值$parent,获得父组件暴露的数据
- 父组件,通过defineExpose暴露自己的数据和方法
<template>
<div>
<h1>$parent</h1>
父组件显示的数字:{{money}}
</div>
<Child1></Child1>
</template>
<script setup lang="ts">
import Child1 from './Child1.vue'
import {ref} from "vue";
const money = ref(10000)
defineExpose({money})
</script>
- 子组件通过标签内的
$parent
获得父组件暴露的数据
<template>
<div>
<button @click="handle($parent)">父组件减10</button>
</div>
</template>
<script setup lang="ts">
const handle = ($parent)=>{
$parent.money -= 10
}
</script>
10.3 总结
- 父组件中,通过
ref
获得整个子组件标签 - 子组件中,通过
$parent
获得整个父组件 - 对方通过
definExpose()
暴露数据,才能获取到
11. defineEmits:自定义事件传递数据
- 父组件:将事件绑定在子组件标签上
<template>
<div>{{info}}</div>
<Event2 @xxx="handler2"></Event2>
</template>
<script setup lang="ts">
import Event2 from './Event2.vue'
import {ref} from "vue";
const info = ref()
function handler2(param1:string,...param2:string[]){
info.value = param1 + param2[0]+param2[1]
}
</script>
- 子组件:通过子组件中的事件触发父组件中绑定的事件,实现数据传递
<template>
<button @click="handle1">子组件传递数据</button>
</template>
<script setup lang="ts">
let emit = defineEmits(['xxx'])
const handle1 = ()=>{
emit('xxx','event2传回的数据1','event2传回的数据2','event2传回的数据3')
}
</script>
12. mitt:数据总线
- 使用ts,报错不断,不会处理
- 安装插件: npm i mitt
- 在src新建目录utils,新建文件event-bus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
emitter.on(eventName, callback) // 绑定事件
emitter.off(eventName, callback) // 关闭事件
emitter.emit(eventName,...args) // 提交事件
emitter.once(eventName, callback) //事件只提交一次
emitter.all.delete(eventName) // 删除事件
emitter.all.clear() // 清除所有事件
- 绑定事件
setup
是最早的生命周期,这里不需要专门写生命周期- 主要就是在mitt上面注册的事件名,发送和接收靠事件名来识别
emitter.on
的第二个参数是回调函数,函数里面的参数是传过来的数据
import emitter from '@/utils/event-bus'
export default {
name : 'app',
components : {User},
setup(){
emitter.on('event1',(info)=>{console.log(info)})
return {}
}
}
- 提交事件,只能发送一个参数
<template>
<button @click="sendInfo">给兄弟组件发送信息</button>
</template>
<script setup lang="ts">
import emitter from '@/utils/event-bus'
const sendInfo = () => {
emitter.emit('event1','晚上去偷菜啊')
}
</script>
13. v-model:实现父子组件数据同步
- 父组件,在组件标签内加入v-model属性
<template>
<div>
<h1>v-model</h1>
父组件显示的金额{{money}}
</div>
<Child v-model="money"></Child>
</template>
<script setup lang="ts">
import Child from '@/views/04_v-model/Child.vue'
import {ref} from "vue";
const money = ref<number>()
money.value = 1000
</script>
- 子组件
- 通过props接收父组件数据,emit给父组件发送数据,
- 特殊的是它们的变量名
modelValue
和update:modelValue
,这两个变量与父组件的v-model
是配套使用的
<template>
<div>
子组件的金额{{modelValue}}
</div>
<button @click="handle">子组件金额-10</button>
</template>
<script setup lang="ts">
let props = defineProps(['modelValue'])
let emit = defineEmits(['update:modelValue'])
const handle = ()=>{
emit('update:modelValue',props.modelValue-10)
}
</script>
- 实现的效果
14. useAttrs :父向子传送标签属性
- 功能和defineProps差不多,也是通过子组件标签的属性传递数据
- 特点:传送后的数据会自动在子组件的标签内展开
示例
- 父组件中引入子组件
<Child>
,子组件的标签内有很多属性
<Child :data1="data1" :title="title1" @click="handle"></Child>
- 子组件内
useAttrs
是vue
原生的方法- 可用通过
useAttrs1.title
访问父组件传过来的数据 - 标签内使用
:="useAttrs1"
,会自动展开所有传过来的属性
<div :title="useAttrs1.title"></div>
<button :="useAttrs1">子组件的按钮</button>
import {useAttrs} from 'vue'
let useAttrs1 = useAttrs();
- 查看
<button>
标签,可以看到<button>
标签内已经将父组件传过来的属性都加在自己的标签内,虽然没有显示@click
属性,但确实可以调用父组件的click
事件
<button data1="子组件执行了click事件" title="父组件提供的标题" >子组件的按钮</button>
完整代码
- 父组件
<template>
<div>
<h1>useAttrs</h1>
{{data1}}
</div>
<Child :data1="data1" :title="title1" @click="handle"></Child>
</template>
<script setup lang="ts">
import Child from '@/views/05_attrs-listeners/Child.vue'
import {ref} from "vue";
const data1 = ref<string>()
const title1 = ref<string>()
data1.value = '父组件的数据1'
title1.value = '父组件提供的标题'
const handle = ()=>{
data1.value = '子组件执行了click事件'
}
</script>
- 子组件
<template>
<div :title="useAttrs1.title">
<button :="useAttrs1">子组件的按钮</button>
</div>
</template>
<script setup lang="ts">
import {useAttrs} from 'vue'
let useAttrs1 = useAttrs();
</script>
需要注意的地方
- 如果子组件中使用了
defineProps
接收了传送过来的某个数据,则useAttrs
就接收不到该数据了
15. provide、inject隔代共享数据
- vue中原生的方法,需要引入
- 类似于消息订阅传递数据
- 只能前辈给后辈传递数据
- 可以隔代共享数据
示例
- 爷爷辈的组件
<template>
<div>
<h1>Provide与Inject</h1>
</div>
爷爷组件:{{data}}
<Child></Child>
</template>
<script setup lang="ts">
import Child from './Child.vue'
import {provide, ref} from "vue";
const data = ref()
data.value = '爷爷辈发送的数据'
provide('info',data)
</script>
- 父亲辈的组件
<template>
<div>
父亲辈组件:{{recData}}
</div>
<button @click="handle">父亲修改数据</button>
<GrandChild></GrandChild>
</template>
<script setup lang="ts">
import GrandChild from './GrandChild.vue'
import {inject} from "vue";
const recData = inject('info');
const handle = ()=> {
recData.value = '儿子修改后的数据'
}
</script>
- 孙子辈的组件
<template>
<div>
孙子辈组件:{{recData}}
</div>
<button @click="handle">孙子修改数据</button>
</template>
<script setup lang="ts">
import {inject, ref} from "vue";
let recData = inject('info');
const handle = ()=> {
recData.value = '孙子修改后的数据'
}
</script>
- 结果
16. pinia :新一代的VueX
16.1 组合式Api
- 安装
npm install pinia
- 在main.js中引入
import { createPinia } from 'pinia'
app.use(createPinia())
- 新建stores目录,新建
.js
文件
pinia的使用和在setup中写的代码基本一致,记得ruturn
数据就行
import {computed, ref} from "vue";
import {defineStore} from 'pinia'
// 第一个参数是 store 的唯一标识id
export const useCounterStore = defineStore('counter',()=>{
// VueX:state,数据
const count = ref(0)
// VueX:getters,计算属性
const doubleCount = computed(() => count.value * 2)
// VueX:actions + mutations,方法
function increment(){
count.value++
}
return { count, doubleCount, increment }
})
- 使用
在vue中导入pinia后需要赋值操作,然后使用.
来访问pinai中的数据和方法
<template>
计数器:{{countStore.count}}<br>
加倍计数器:{{countStore.doubleCount}}<br>
<button @click="countStore.increment">点我加1</button>
</template>
<script setup>
import {useCounterStore} from "@/stores/counter"
const countStore = useCounterStore()
</script>
- 另一种使用方式,对store解构,不使用
countStore.
去访问store中的数据和方法
<template>
计数器:{{count}}<br>
加倍计数器:{{doubleCount}}<br>
<button @click="increment">点我加1</button>
</template>
<script setup>
import {useCounterStore} from "@/stores/counter"
import {storeToRefs} from "pinia";
// 得到store的实例对象
const countStore = useCounterStore()
const { count, doubleCount } = storeToRefs(countStore)
const {increment} = countStore
</script>
16.2 pinia-plugin-persistedstate : 数据持久化
- 安装
npm : npm i pinia-plugin-persistedstate
main.js
中pinia使用插件
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia} from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() // 创建pinia实例
pinia.use(piniaPluginPersistedstate) // pinia使用持久化插件
createApp(App).use(pinia).mount('#app')
- 修改pinia的stroe文件,最简单粗暴的用法,将上面的state数据保存在localstorage(关闭浏览器数据清空)
查看保存在localstorage中的结果
- 如果想保存到session storage中(session storage关闭页面丢失数据)
只需要给persist加个对象,paths中是要保存的数据的名字
persist: {
storage: sessionStorage,
paths:['count']
}
17. 插槽
- 不使用插槽的情况下
结果:
17.1 默认插槽
结果:
17.2 具名插槽
#b
是v-slot:b
的缩写
顾名思义就是指着名字去插入
结果:
17.3 作用域插槽
- 可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传数据是以何种结构或者外观在子组件内部去展示
在父组件中也可以通过v-slot进行解构
<template>
<test3 :data="testData">
<template v-slot="{$a,b}">
<span :style="{backgroundColor:$a.todo?'red':'blue'}">
第{{b}}条数据:内容是{{ $a }}
</span>
</template>
</test3>
</template>