007setup
概述
-
setup
是一个函数 -
模板需要的数据和方法写在
setup
函数内部 -
需要在函数的最后
return(){}
返回模板中需要的数据和方法的名字 -
不可以在里面写
this
,它是undefin
,vue3弱化this -
此时页面不是响应式的
-
setup的执行比
beforeCreate()
还早
008setup
的返回值
-
如果在
setup
中返回一个函数里再返回内容,那么它将无视模板写的内容,直接渲染此内容return ()=>'哈哈'
009setup
和OptionsAPI
的区别
-
data(){}
,methors(){}
可以和setup(){}
一起写(同级),但是尽量不要 -
旧写法(vue2)可以读到新写法(vue3)的东西,但新写法(vue3)可以读到旧写法(vue2)的东西
010setup
的语法糖
<script setup>
let a = 1
</script>
相当于
<script>
setup() {
let a = 1
return {a}
}
</script>
011ref
创建__基本类型的响应式数据
-
刚开始要从vue中引入ref
import {ref} from 'vue'
-
想要响应式数据的数据要用
ref()
包起来<template> <div class="person"> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> <h2>地址:{{address}}</h2> <button @click="changeName">修改名字</button> <button @click="changeAge">修改年龄</button> <button @click="showTel">查看联系方式</button> </div> </template> <script lang="ts" setup name="Person"> import {ref} from 'vue' let name = ref('张三') let age = ref(18) let tel = '13888888888' let address = '北京' // 方法 function changeName() { name.value = 'zhang-san' console.log(name.value) } function changeAge() { age.value += 1 console.log(age.value) } function showTel() { alert(tel) } </script>
-
被
ref()
包起来的东西变成了RefImpl{}
的实例对象,里面有value
-
模板里不用
.value
,,JS里操作ref()
包起来的东西一定需要.value
012reactive
创建_对象类型的响应式数据
-
刚开始要从vue中引入
reactive
import {reactive} from ‘vue’
-
想要响应式数据的数据要用
reactive()
包起来 -
被
reactive()
包起来的东西变成了windowns本来就有的Proxy{}
的函数 -
reactive
是深层次的响应式
013ref
创建_对象类型的响应式数据
ref
可以定义基本类型和对象类型的响应式数据
reactive
只能定义对象类型的响应式数据
-
ref
实现响应式数据底层是用reactive
实现:被ref()
包起来的东西变成了RefImpl{}
的实例对象,但是RefImpl{}
的实例对象里又包着Proxy{}
的函数 -
所以
ref
的处理也是是深层次的响应式
014ref
对比reactive
-
使用了
reactive
后可以改它里面的里面的某属性,但是不可以全部改了,如果全部改了那么它的响应式就会消失-
错误例子:
<template> <div class="person"> <button @click="changeCar">修改汽车</butto> </div> </template> <script lang="ts" setup name="Person"> import {ref,reactive} from 'vue' let car = reactive({brand:'奔驰',price:100}) function changeCar(){ car = {brand:'奥拓',price:1} //这么写页面不更新的 car = reactive({brand:'奥拓',price:1}) //这么写页面不更新的 } </script>
-
正确例子:解释:把新的
brand:'奥拓'
分配给原来的brand
,把新的price:1
分配给原来的price
,相当于批量地把旧的数据替换成新的数据,所以对象的地址是不变的<template> <div class="person"> <button @click="changeCar">修改汽车</butto> </div> </template> <script lang="ts" setup name="Person"> import {ref,reactive} from 'vue' let car = reactive({brand:'奔驰',price:100}) function changeCar(){ Object.assign(car,{brand:'奥拓',price:1}) } </script>
-
-
使用了
ref
后可以改它里面的里面的某属性,也可以全部改了,它的响应式不会消失注意:使用
ref
一定要有.value
去改 -
使用区别:
-
基本类型的响应式数据———>
ref
-
响应式对象且层级不深———>
reactive
-
响应式对象且层级较深———>
ref
-
015toRefs
与toRef
-
从一个响应式对象解构东西的时候,解构出来的相应的属性都不是响应式的,相当于拿了响应式的数据又创建一个新的地址来存放此数据
-
解决方法:
-
用
toRefs
把需要解构的对象包起来,里面的属性变成ref
所定义的的响应式数据姓名:{{person.name}}
年龄:{{person.age}},{{nl}}
-
注意:使用
toRefs
前也要先从vue
中引用
-
toRefs
解构对象,`toRef``解构对象中的某个属性let nl = toRef(person,'age')
016computed
计算属性
-
刚开始要从vue中引入
computed
-
computed
计算属性有缓存,但是方法没有缓存 -
如果想要
computed
计算出来的数据既可以读,也可以写,那么它里面就要又get(){}
和set(){}
let fullName = computed({ get(){ return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value }, set(val){ const [str1,str2] = val.split('-') firstName.value = str1 lastName.value = str2 } })
-
computed
计算出来的是一个ref
定义的响应式数据
017watch
监视_情况一 :监视ref
定义的【基本类型】数据
-
watch
能监视三种数据:-
具有响应式的数据(由
ref
或reactive
定义) -
函数返回的一个值(
getter
函数) -
一个包含上述内容的数组
-
-
监视
ref
定义的数据的时候不用.value
-
watch
可以传入两个数据,一个是改了之后的数据,另一个改了前的数据watch(sum,(newValue,oldValue)=>{ ...... } })
-
解除监视:
watch
调用的返回值就是停止监视的函数,调用此函数则监视停止const stopWatch = watch(sum,(newValue,oldValue)=>{ if(......){ stopWatch() } })
018watch
监视_情况二:监视ref
定义的【对象类型】数据
-
它监视的是整个对象的地址值,如果改变整个对象,则相当改了它的地址值,会被监视到;如果只改变此对象的其中的某个属性的值,此改变无法被监视到,因为它的地址值没有变,此时新数值和旧数值相同。除非开启深度监视,都可以监视到
...... let person = ref({ name:'张三', age:18 }) ...... watch(person,(newValue,oldValue)=>{ ...... },{deep:true}) ......
019watch
监视_情况三:监视reactive
定义的【对象类型】数据
- 默认开启深度监视,无法关闭
020watch
监视_情况四
-
如果只想要监视响应式对象(
ref
或reactive
)里的某一个属性值(不是对象),则使用一个getter(){}
包起来(写成函数形式)...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch(()=>{return person.car},(newValue,oldValue)=>{ console.log('person.car变化了',newValue,oldValue) },{deep:true}) ......
可以简写成:
...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch(()=>person.car,(newValue,oldValue)=>{ console.log('person.car变化了',newValue,oldValue) },{deep:true}) ......
-
如果只想要监视响应式对象(
ref
或reactive
)里的属性值(对象)-
如果写成函数式,则监视的是地址值,则里面的某个属性值的变化无法被监视
...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch(()=>person.car,(newValue,oldValue)=>{ console.log('person.car变化了',newValue,oldValue) }) ......
-
如果写成普通式,则监视的是里面的某个属性值,则地址的(整体)变化无法被监视
...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch(person.car,(newValue,oldValue)=>{ console.log('person.car变化了',newValue,oldValue) }) ......
-
-
解决方法:,则建议使用一个
getter(){}
包起来(写成函数形式),并且手动开启深度监视,则无论是整体(地址值)还是部分的改变都可以被监视到...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch(()=>person.car,(newValue,oldValue)=>{ console.log('person.car变化了',newValue,oldValue) },{deep:true}) ......
021watch
监视_情况五:监视多数据
-
方法:
-
如果监视的是基本数据类型,则需要写成一个函数
...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{ ...... },{deep:true}) ......
-
如果监视的是对象类型,则写普通写法
...... let person = reactive({ name:'张三', age:18, car:{ c1:'奔驰', c2:'宝马' } }) ...... watch([()=>person.name,person.car],(newValue,oldValue)=>{ ...... },{deep:true}) ......
注意:怎么写就要看你监视的数据类型是什么…
-
022watchEffect
-
刚开始要从vue中引入
watchEffect
-
watchEffect
和watch
的区别:-
watch
需要指定监视谁,但是watchEffect
可以自己通过判断进行自动的监视 -
watch
需要写立即执行的属性一运行就调用一次,但是watchEffect
中没有写立即执行的属性,它一运行就自己调用一次
-
-
例子:
-
watch
实现:watch([temp,height],(value)=>{ let [newTemp,newHeight] = value if(newTemp >= 60 || newHeight >= 80){ console.log('给服务器发请求') } })
-
watchEffect
实现:watchEffect(()=>{ if(temp.value >= 60 || height.value >= 80){ console.log('给服务器发请求') } })
-
023标签的ref
属性
-
使用情况:因为在Vue3中可能会有很多的组件,而组件里又有很多的属性和标识,为了避免在不同的组件下恰好使用了相同的标签和
id
引发的混乱,所以使用ref
属性让它们在各自的组件中发挥作用 -
如果在模板中使用
ref
属性当标识,一定要在脚本中创建一个用来储存内容的东西 -
在style标签中使用
scoped
作用:如果一模一样的选择器和文字,但是不想发生混乱。使用了scoped
让样式变成局部的,一定要加 -
把
ref
属性加在组件标签上并打印出来是组件实例对象,并且在没有加defineExpose
的情况下无法在此实例对象拿到自己先前定义的东西,如果想拿到就要在脚本里加defineExpose
,并且记得从vue中引用import {ref} from 'vue' ...... let title2 = ref() let a = ref(0) let b = ref(1) let c = ref(2) ...... defineExpose({a,b,c}) ......
024回顾一下TS
-
接口:写一个接口去规范每一个属性,要在使用的地方进行引入
export interface PersonInter { id:string, name:string, age:number } ...... import {type PersonInter} from '@/types' ...... let person:PersonInter = {id:'asyud7asfd01',name:'张三',age:60} ......
-
泛型:处理多种类型
export interface PersonInter { id:string, name:string, age:number } export type Persons = Array<PersonInter> ...... import {type PersonInter,type Persons} from '@/types' ...... let personList:Persons = [ {id:'asyud7asfd01',name:'张三',age:60}, {id:'asyud7asfd02',name:'李四',age:18}, {id:'asyud7asfd03',name:'王五',age:5} ] ......
025props
的使用
-
props
概念:是用于组件之间传递数据的一种机制。组件可以通过props
接收父组件传递过来的数据,从而实现组件的复用和数据的共享。 -
形式:
-
只接收(无法在子组件中打印)解释:接收父组件传递过来的
list
defineProps(['list'])
-
接收,同时将props保存起来解释:接收父组件传递过来的
list
并保存,此时x为对象,对象储存的是从父组件传递过来的数据defineProps(['list']) let x = defineProps(['list'])
-
接收+ 限制类型(
defineProps
可以传泛函)解释:接收父组件传递过来的list
并限制为Persons
类型defineProps<{list:Persons}>()
-
接收 + 限制类型 + 限制必要性 + 指定默认值解释:接收父组件传递过来的可选的
list
并限制为Persons
类型,若父组件没有传递list
prop
,组件会使用默认的人员列表进行渲染;若父组件传递了list
prop
,则使用父组件传递的值。withDefaults(defineProps<{list?:Persons}>(),{ list:()=> [{id:'ausydgyu01',name:'康师傅·王麻子·特仑苏',age:19}] })
-
注意:
1.父组件传递过来的时候写的是什么,那么子组件也得写什么字符串;
2.父组件中根据情况加冒号;
3.在
<script setup>
语法糖里使用开头有define
的都为宏函数(宏函数在编译时会被直接替换为其定义的代码,而不是像普通函数那样在运行时调用),不用从vue中引用了
026生命周期
-
生命周期(生命周期函数,生命周期钩子)概念:不同时期调用不同的函数
-
vue2
:四个阶段(创建,挂载,更新,销毁)-
创建:
beforeCreate() {......}
,created() {......}
-
挂载:
beforeMount() {......}
,mounted() {......}
-
更新:
beforeUpdate() {......}
,updated() {......}
-
销毁:
beforeDestroy() {......}
,destroy() {......}
-
-
vue3
:四个阶段(创建,挂载,更新,卸载)-
创建:
setup
-
挂载:
onBeforeMount(() => {......})
,onMounted(() => {......})
-
更新:
onBeforeUpdate(() => {......})
,onUpdated(() => {......})
-
销毁:
onBeforeUnmount(() => {......})
,onUnmounted(() => {......})
注意: 1.
vue3
的生命周期函数使用前都要从vue
中引用; 2.App
组件最后挂载(必须等所有组件挂载完); 3.常用的生命周期函数:onMounted(() => {......})
(挂载完毕);onUpdated(() => {......})
(更新完毕);onBeforeUnmount(() => {......})
(销毁前)
-
027自定义hooks
-
hooks
文件里的useSum.ts
解释:使用的数据和方法用函数包裹起来并暴露出去,最后返回需要的东西import { ref ,onMounted,computed} from 'vue' export default function () { let sum = ref(0) let bigSum = computed(()=>{ return sum.value * 10 }) function add() { sum.value += 1 } onMounted(()=>{ add() }) return {sum,add,bigSum} }
-
components
文件里的Person.vue
解释:从相应的文件里引用并结构去使用<script lang="ts" setup name="Person"> import useSum from '@/hooks/useSum' import useDog from '@/hooks/useDog' const {sum,add,bigSum} = useSum() const {dogList,getDog} = useDog() </script>
注意:
1.
hooks
命名规范:开头一定要是use
;2.模块化思想:脚本里相应的数据的方法放在不同的文件里
028 路由
-
前端使用路由的目的:前端使用路由是为了实现单页面应用(SPA应用)
-
路由本质:就是一组key(通常是指路由的路径(如
/home
、/news
等))-value(是与该路径对应的组件(如Home.vue
、News.vue
等组件))的对应关系 -
路由器的作用:多个路由需要路由器的管理
-
route规则:由路径寻找相应的组件
-
流程:点击——>路径变化——>路由器捕获——>寻找相应的route规则——>把相应的组件挂载到对应的地方
-
流程:
-
1.写出相应的区域:要在根组件(通常是
App.vue
)里预留出路由视图显示的区域<template> <div class="app"> <h2 class="title">Vue路由测试</h2> <!-- 导航区 --> <div class="navigate"> <RouterLink to="/home" active-class="xiaozhupeiqi">首页</RouterLink> <RouterLink to="/news" active-class="xiaozhupeiqi">新闻</RouterLink> <RouterLink to="/about" active-class="xiaozhupeiqi">关于</RouterLink> </div> <!-- 展示区 --> <div class="main-content"> <RouterView></RouterView> </div> </div> </template> <script lang="ts" setup name="App"> import { RouterView, RouterLink } from 'vue-router';</script> <style> ...... </style>
-
RouterLink
:Vue Router
提供的一个组件,用于在Vue
应用中创建导航链接。它的主要作用是在不刷新整个页面的情况下,实现单页面应用(SPA)内的路由切换。 -
to
属性:是<RouterLink>
组件与编程式导航里非常关键的属性,用于指定目标路由的信息-
字符串写法:直接指定路径
to="/home
-
对象写法:
to="{ name: 'ProductDetail', params: { id: 123 }, query: { sort: 'asc' } }
-
-
active-class
:是 Vue Router 中<RouterLink>
组件的一个属性,其主要作用是在链接对应的路由处于激活状态时,为该链接添加指定的 CSS 类名,这样就可以通过 CSS 样式来突出显示当前激活的导航链接 -
RouterView
:是 Vue Router 里的一个关键组件,其作用为根据当前的路由匹配结果,在指定位置渲染对应的组件
-
-
-
2.配置路由器:需要创建一个路由器实例,并且引入 Vue Router。在 Vue 项目里,通常会单独创建一个
router
文件夹,在其中的index.js
文件里进行路由器的配置。import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; import Home from '@/pages/Home.vue'; import News from '@/pages/News.vue'; import About from '@/pages/About.vue'; const router = createRouter({ history: createWebHistory(), routes: [ { path: '/home', component: Home }, { path: '/news', component: News }, { path: '/about', component: About } ] }); export default router;
-
3.制定路由的具体规则(路径对应组件):要在路由器配置文件里定义路由规则,也就是将路径和对应的组件关联起来。
-
命名路由:为路由设置一个名称,方便在编程式导航或其他地方引用
const router = createRouter({ history: createWebHistory(), routes: [ { name: 'zhuye', path: '/home', component: Home }, ...... ] });
通过命名路由,可以使用
router.push({ name: 'zhuye' })
进行编程式导航 -
嵌套路由:在一个路由组件内部再进行路由匹配和组件渲染
import { createRouter, createWebHistory } from 'vue-router'; import Home from '@/pages/Home.vue'; import News from '@/pages/News.vue'; import About from '@/pages/About.vue'; import Detail from '@/pages/Detail.vue'; const router = createRouter({ history: createWebHistory(), routes: [ { name: 'zhuye', path: '/home', component: Home }, { name: 'xinwen', path: '/news', component: News, children: [ { name: 'xiang', path: 'detail', component: Detail, props(route) { return route.query; } } ] }, { name: 'guanyu', path: '/about', component: About } ] }); export default router;
-
query
参数:以查询字符串的形式附加在 URL 后面,如/news?id=1&title=abc
,在组件中可以通过$route.query
来获取。常用于传递一些非必要的、不影响路由匹配的参数 -
params
参数:是路由路径的一部分,如/user/:id
,:id
就是一个参数占位符,在组件中通过$route.params
获取。params 参数会影响路由的匹配,当路径参数发生变化时,组件会重新渲染 -
props
配置:用于将路由参数作为props
传递给组件-
props: true
:将路由收到的所有params
参数作为props
传给路由组件 -
函数写法:如
props(route) { return route.query; }
,可以根据需求灵活决定将什么作为props
传递给组件 -
对象写法:如
props: { a: 100, b: 200, c: 300 }
,直接指定固定的属性值作为props
传递给组件
-
-
-
4.形成
main.js
文件import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; const app = createApp(App); app.use(router); app.mount('#app');
-
其他:
-
replace 属性:在
<RouterLink>
组件中使用replace
属性,如<RouterLink replace to="/home">首页</RouterLink>
,表示导航时会替换当前的历史记录条目,而不是添加新的记录。这在某些场景下可以避免用户通过浏览器的返回按钮回到之前的页面 -
编程式路由导航:除了使用
<RouterLink>
组件进行导航外,还可以通过编程的方式进行路由导航。常见的方法有:-
router.push(location)
:向 history 栈中添加一个新的记录,实现页面跳转。location
可以是字符串路径,也可以是一个路由对象。 -
router.replace(location)
:替换 history 栈中的当前记录,实现页面跳转 -
router.back()
:返回历史记录中的上一个页面 -
router.forward()
:前进到历史记录中的下一个页面
-
-
重定向:可以在路由配置中设置重定向规则,将一个路径重定向到另一个路径
const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', redirect: '/home' }, ...... ] });
-
029 pinia
-
概念:把多个组件需要共用的数据统一存在同一个仓库里
-
state:通过
defineStore
定义, 包含: State、Getters、Actions-
State:最开始的数据,相当data
-
Getters:对数据的加工和计算,相当computed
-
Actions:操作方法,加或减等,相当methods
注意:State、Getters、Actions 和 Vue 组件中 data、computed、methods的区别:组件的 data、computed、methods 都是 局部的,但是pinia的State、Getters、Actions是
-
-
储存数据:
-
在
state
或reactive
中定义数值-
选项式 API:
state: () => ({ sum: 3, school: 'atguigu' })
-
组合式:
const talkList = reactive([])
-
-
用
$subscribe
监听 Store 变化,把数据存到localStorage
中注意:初始化时需从本地读取数据
state: () => ({ talkList: JSON.parse(localStorage.getItem('talkList') || '[]') })
talkStore.$subscribe((mutation, state) => { localStorage.setItem('talkList', JSON.stringify(state.talkList)) })
-
-
读取数据:
-
在组件中通过
useStore()
拿到 Store ,用storeToRefs
解构有响应式的数据const countStore = useCountStore() const { sum, school } = storeToRefs(countStore)
-
-
修改数据
-
第一种(直接改):直接赋值
countStore.sum -= n.value;
注意:对象 /或数组不可以用这个方法,因为会失去响应式,应该整体替换或用
$patch
整体:// 错误的例子 countStore.obj.name = 'new' // 正确的例子 countStore.obj = { ...countStore.obj, name: 'new' }
$patch
:countStore.$patch({ sum: 10, school: '尚硅谷' })
-
第二种(改多个):用
$patch
把状态全改了countStore.$patch({ sum: 10, school: '尚硅谷' })
-
第三种(通过 Actions ):把修改的代码写在 Store 的
actions
中注意:不要解构
state
,会失去响应式,改用storeToRefs
解包//错误例子 const { count } = state
storeToRef
:一个属性解包,有响应式const count = storeToRef(store, 'count')
storeToRefs
:批量解包所有属性,每个都是ref
const { count, name } = storeToRefs(store)
actions: { increment(value) { if (this.sum < 10) this.sum += value; } } <button @click="countStore.increment(n.value)">加</button>
-
-
$subscribe:监听 Store 中任意状态的变更,执行副作用(存到本地存储或打印日志…)
talkStore.$subscribe(() => { localStorage.setItem('talkList', JSON.stringify(talkStore.talkList));
030 组件通信
-
props
通信(父传子):父组件向子组件传递数据。父组件在使用子组件标签时绑定数据,子组件通过defineProps
声明接收的属性。注意:
-
props
传递数据是单向的,只能父组件传给子组件。子组件可不能直接去改props
里的值。要是子组件觉得这数据要改,就在父组件去改 -
如果用了
TypeScript
,就得把props
的数据类型明确规定好
// 父组件 <template> <div class="father"> <Child :message="parentMessage" /> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue'; import { ref } from 'vue'; const parentMessage = ref('Hello from parent'); </script> // 子组件 <template> <div class="child"> <h4>{{ message }}</h4> </div> </template> <script setup lang="ts" name="Child"> const props = defineProps(['message']); </script>
-
-
自定义通信(子传父):子组件向父组件传递数据。子组件通过
defineEmits
声明事件,使用emit
触发事件并传递数据,父组件在使用子组件标签时监听事件注意:
-
事件的名字一定要起得独特,好理解,最好名字能一下子看出来这个事件是干什么的
-
子组件传的数据要和父组件这边处理函数能接受的数据类型匹配上,不然父组件收到数据也没法正常用
// 子组件 <template> <div class="child"> <button @click="sendData">发送数据</button> </div> </template> <script setup lang="ts" name="Child"> const emit = defineEmits(['childEvent']); const sendData = () => { emit('childEvent', 'Data from child'); }; </script> // 父组件 <template> <div class="father"> <Child @childEvent="handleChildEvent" /> <h4>{{ receivedData }}</h4> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue'; import { ref } from 'vue'; const receivedData = ref(''); const handleChildEvent = (data: string) => { receivedData.value = data; }; </script>
-
-
mitt
库实现跨组件通信:使用mitt
库创建一个事件总线,不同组件通过事件总线发布和订阅事件来实现通信注意:
-
要管理好这些事件。用完事件之后,要及时把它关掉,不然一直开着会占内存。比如在组件不用了要卸载的时候,就把相关事件解绑数据
-
清楚数据的流向。要知道哪个组件是发布消息的,哪个组件是接收消息的
// 创建事件总线 import mitt from'mitt'; const emitter = mitt(); // 组件 A import { onMounted } from 'vue'; onMounted(() => { emitter.on('customEvent', (data) => { console.log('Received data in Component A:', data); }); }); // 组件 B import { onMounted } from 'vue'; onMounted(() => { emitter.emit('customEvent', 'Data from Component B'); });
-
-
v-model
双向数据绑定:语法糖:v-model
是:value
和@input
的语法糖,用于实现父子组件之间的双向数据绑定。父组件使用v-model
绑定数据,子组件通过defineProps
接收modelValue
,通过defineEmits
触发update:modelValue
事件注意:
-
要是一个组件里用了好几个
v-model
,要把不同的modelValue
和对应的update:modelValue
事件处理好 -
v-model
本质上就是:value
和@input
的语法糖
// 父组件 <template> <div class="father"> <Child v-model="parentValue" /> <h4>{{ parentValue }}</h4> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue'; import { ref } from 'vue'; const parentValue = ref('Initial value'); </script> // 子组件 <template> <div class="child"> <input :value="modelValue" @input="updateValue" /> </div> </template> <script setup lang="ts" name="Child"> const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); const updateValue = (e: any) => { emit('update:modelValue', e.target.value); }; </script>
-
-
$attrs
透传(父传子再传孙):父组件传递给子组件的属性,如果子组件没有在props
中声明,这些属性会被$attrs
接收,子组件可以通过v-bind="$attrs"
将这些属性透传给孙组件注意:
-
子组件有时候可能不想让
$attrs
里的属性自动绑到自己的根元素上,这时候就得设置inheritAttrs: false
,然后按需用v-bind="$attrs"
把属性透传给孙组件 -
要是子组件的
props
和$attrs
里有相同的属性,这时候props
里接收到的那个属性会被优先用,因为props
的优先级更高
// 父组件 <template> <div class="father"> <Child :a="1" :b="2" :custom="customProp" /> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue'; import { ref } from 'vue'; const customProp = ref('Custom data'); </script> // 子组件 <template> <div class="child"> <GrandChild v-bind="$attrs" /> </div> </template> <script setup lang="ts" name="Child"> import GrandChild from './GrandChild.vue'; </script> // 孙组件 <template> <div class="grand-child"> <h4>a: {{ a }}</h4> <h4>b: {{ b }}</h4> <h4>custom: {{ custom }}</h4> </div> </template> <script setup lang="ts" name="GrandChild"> const props = defineProps(['a', 'b', 'custom']); </script>
-
-
$parent和$refs
:-
parent
:子组件通过parent
访问父组件的实例,从而访问父组件的属性和方法。但不推荐过度使用,会导致组件耦合度增加 -
refs
:父组件通过给子组件添加引用,然后通过refs
访问子组件的实例,进而访问子组件的属性和方法注意:
-
使用
$parent
会增加组件间的耦合度,尽量减少使用 -
使用
$refs
时,要考虑组件的生命周期,确保在合适的时机访问$refs
// 子组件 <template> <div class="child"> <h4>子组件数据: {{ childData }}</h4> </div> </template> <script setup lang="ts" name="Child"> import { ref } from 'vue'; const childData = ref('Child data'); </script> // 父组件 <template> <div class="father"> <Child ref="childRef" /> <button @click="accessChildData">获取子组件数据</button> <h4>从子组件获取的数据: {{ receivedChildData }}</h4> </div> </template> <script setup lang="ts" name="Father"> import Child from './Child.vue'; import { ref } from 'vue'; const receivedChildData = ref(''); const childRef = ref(); const accessChildData = () => { if (childRef.value) { receivedChildData.value = childRef.value.childData; } }; </script>
-
-
031 插槽通信
-
默认插槽(匿名插槽):最基础的插槽形式。父组件写在子组件标签内的内容,会渲染在子组件
<slot>
标签处;若父组件未提供内容,子组件可展示<slot>
内默认内容(父组件提供数据,子组件提供结构)// 子组件 <template> <div class="child"> <h2>子组件</h2> <slot>这是子组件默认插槽的默认内容</slot> </div> </template> <script setup lang="ts"> </script> // 父组件 <template> <div class="parent"> <h2>父组件</h2> <tChild> <p>这是父组件提供给子组件默认插槽的内容</p> </Child> </div> </template> <script setup lang="ts"> import Child from './Child.vue'; </script> <style scoped> ...... </style>
注意:
-
父组件传入内容会覆盖子组件默认插槽的默认内容
-
一个组件通常仅一个默认插槽,因为如果有多个默认插槽则数据不知道放在哪个默认插槽中,因为默认插槽没有独特的名字
-
-
具名插槽:子组件可定义多个具名插槽,父组件按名称将数据放到对应的位置,实现灵活
// 子组件 <template> <div class="child"> <h2>子组件</h2> <slot name="header">默认头部内容</slot> <slot name="body">默认主体内容</slot> <slot name="footer">默认底部内容</slot> </div> </template> <script setup lang="ts"> </script> // 父组件 <template> <div class="parent"> <h2>父组件</h2> <Child> <template v-slot:header> <h3>父组件提供的头部内容</h3> </template> <template v-slot:body> <p>父组件提供的主体内容</p> </template> <template v-slot:footer> <p>父组件提供的底部内容</p> </template> </Child> </div> </template> <script setup lang="ts"> import Child from './Child.vue'; </script> <style scoped> ...... } </style>
注意:
-
插槽的名字要有唯一性并且清楚,方便父组件精准放到相应的位置
-
子组件可为具名插槽设置默认内容,当父组件未传数据的时侯显示默认的内容
-
父组件可用
<template v-slot:插槽名>
或#插槽名
(语法糖)两种语法绑定内容
-
-
作用域插槽:子组件在
<slot>
标签绑定属性,将数据传给父组件;父组件使用插槽时,通过解构获取数据并自定义渲染注意:
-
子组件传递给插槽的属性是响应式的,如果子组件里的数据发生了变化,那么父组件的展示内容会自动更新
-
父组件解构插槽数据的时候,名称和结构需和子组件传递的数据一样
-
-
场景:1.默认插槽:用于父组件向子组件插入单一内容,如通用容器组件填充文本、图片等2.具名插槽:子组件有多个功能区域需父组件填充内容时使用,如布局组件的头部、主体、底部3.作用域插槽:子组件数据需在父组件按需展示或处理时使用,如列表组件的数据自定义渲染
032 其他API
-
shallowRef与shallowReactive:
-
shallowRef:用于创建一个浅层响应式数据引用,它只关注这个数据整体有没有被替换成新的值,而不会对值内部的属性变化进行追踪
<template> <div> <h2>求和为:{{ sum }}</h2> <button @click="changeSum">sum+1</button> </div> </template> <script setup lang="ts"> import { shallowRef } from 'vue'; let sum = shallowRef(0); function changeSum() { sum.value += 1; } </script>
注意:
-
通过
.value
来改shallowRef
存的数据 -
如果
shallowRef
存的是个对象,直接改对象里面的某个属性,页面是不会更新的,因为对象整体并没有被换成新的对象
-
-
shallowReactive:创建一个浅层响应式对象,它只会对对象的第一层属性进行响应式处理,不会对里面再嵌套的那些对象的属性进行响应式转换
<template> <div> <h2>汽车为:{{ car }}</h2> <button @click="changeBrand">修改品牌</button> </div> </template> <script setup lang="ts"> import { shallowReactive } from 'vue'; let car = shallowReactive({ brand: '奔驰', options: { color: '红色', engine: 'V8' } }); function changeBrand() { car.brand = '宝马'; } </script>
注意:
-
要是改这个浅层响应式对象最外面那层的属性,页面就能跟着更新,但要是改里面嵌套对象的属性,页面不会自动刷新
-
与
shallowRef
很像
-
-
-
readonly和shallowReadonly:
-
readonly:创建一个只读(所有属性都变成只读)的响应式对象,这个对象既不能整个被重新赋值,里面的属性也没法修改
<template> <div> <h2>当前sum1为:{{ sum1 }}</h2> <h2>当前sum2为:{{ sum2 }}</h2> <button @click="changeSum1">点我sum1+1</button> </div> </template> <script setup lang="ts"> import { ref, readonly } from 'vue'; let sum1 = ref(0); let sum2 = readonly(sum1); function changeSum1() { sum1.value += 1; // 可以修改 // sum2.value += 1; // 报错,sum2是只读的 } </script>
注意:
- 如果
readonly
包裹的是一个可变的引用,可以通过修改原始数据间接影响readonly
对象
- 如果
-
shallowReadonly:创建一个浅层只读的响应式对象,对象最外面那一层的属性变成只读的(最外面那一层的属性变成只读),但是可以修改对象里面嵌套的那些对象的属性
<template> <div> <h2>当前car1为:{{ car1 }}</h2> <h2>当前car2为:{{ car2 }}</h2> <button @click="changeBrand1">修改品牌(car1)</button> </div> </template> <script setup lang="ts"> import { reactive, shallowReadonly } from 'vue'; let car1 = reactive({ brand: '奔驰', options: { color: '红色', price: 100 } }); let car2 = shallowReadonly(car1); function changeBrand1() { car1.brand = '宝马'; // 可以修改car1的属性 // car2.brand = '奥迪'; // 报错,car2的第一层属性是只读的 car2.options.price = 110; // 可以修改car2嵌套对象的属性 } </script>
注意:
- 改
shallowReadonly
对象最外面那层的属性程序会报错,但是可以修改它里面嵌套对象的属性
- 改
-
-
toRow和markRaw:
-
toRow:从一个经过 Vue 处理的响应式对象里拿到它原本的样子,也就是还没被 Vue 加上响应式功能的那个对象
<template> <div> <h2>姓名:{{ person.name }}</h2> <h2>年龄:{{ person.age }}</h2> <button @click="person.age += 1">修改年龄</button> </div> </template> <script setup lang="ts"> import { reactive, toRaw } from "vue"; let person = reactive({ name: 'tony', age: 18 }); let rawPerson = toRaw(person); rawPerson.age = 20;// 直接操作原始对象,不会触发响应式更新 </script>
注意:
-
如果用
toRaw
拿到原始对象后对这个对象做修改的时候,页面是不会自动跟着更新的 -
别老是用
toRaw
,因为它破坏了 Vue 响应式系统正常的运行流程
-
-
markRaw:给一个对象做个 “标记”,这样 Vue 就不会把这个对象变成响应式对象
<template> <div> <h2>{{ car }}</h2> </div> </template> <script setup lang="ts"> import { reactive, markRaw } from "vue"; let car = markRaw({ brand: '奔驰', price: 100 });//// car 对象不会被 Vue 转换成响应式对象 let car2 = reactive(car);//依然不具备响应式特性 </script>
注意:
-
被
markRaw
标记过的对象没有响应式特性,如果改了它里面的属性,页面不会自动更新的 -
如果把
markRaw
标记过的对象还当成一个响应式对象的属性,那这个属性同样也不会有响应式的效果
-
-
-
customRef:它能让开发者自己创建响应式引用,可以按照自己的想法去定义数据读取(
get
)和写入(set
)时的操作<template> <div> <h2>{{ msg }}</h2> <input type="text" v-model="msg"> </div> </template> <script setup lang="ts"> import { customRef } from "vue"; function useMsgRef(initValue: string, delay: number) { let timer: number; let msg = customRef((track, trigger) => { return { get() { track(); return initValue; }, set(value) { clearTimeout(timer); timer = setTimeout(() => { initValue = value; trigger(); }, delay); } }; }); return { msg }; } let { msg } = useMsgRef('你好', 2000); </script>
注意:
- 在
customRef
的get
方法里,要调用track
函数。在set
方法里呢,得调用trigger
函数
- 在