vue3张天禹

变化
1
2
3
4
1性能提升:打包内存渲染 
2proxy取代defineProperty 、重写虚拟dom
3ts语法
4新特性:组合api、ref、setup、computed/watch、碎片组件
创建工程
1
2
3
4
5
6
7
项目创建命令,分vue-cli使用vue ui和vite官网命令 2种
npm create vue@latest
npm i vant

webpack--entry--每个路由-每个模块--再启动
vite--直接启动--ertry--指定路由-指定模块
vite优点:热重载、支持ts、jsx、按需编译
文件目录
1
2
3
.vscode插件、
env.d.ts让项目认识各种类型的文件、
index.html入口文件,vue-cli一般是main.js做入口文件,vite.config.ts各种插件的配置
setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setup是和data、methods等级的一个配置项,执行直接比beforeCreate还早

setup(){
const name="lxc"
const showName=()=>{alert(name)}
return {name,showName}
}
1 setup里不能用this
2 setup不一定返回对象,也可以返回函数-比如返回jsx直接渲染

setup(){
return ()=>JSX
}
3 setup和data的区别
setup和data能同时存在,data能读取到setup里的数据,setup里不能用data里的数据
语法糖
1
2
3
4
5
6
7
8
语法糖写法不能写vue2,也不能给组件起名了,除非分开写2个script

npm i vite-plugin-vue-setup-extend -d
在vite.config.js中配置后,就可以给组件起别名
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
在pluguns中添加VueSetupExtend()

<script setup lang='ts' name='AAA'>
响应式
1
2
3
4
5
6
7
8
9
10
const name = ref('张三')

const data = reactive({name:'lxc',age:18}) 只用于对象类型

1 控制台打印出来都是Proxy对象

对于数组,如果用reactive,要改变data的值不能直接赋值,要逐一push

2 js中使用ref的响应数据,要加.value,如name.value, obj.value.name,但是打印可以不加value
const obj = ref({name:'lxc',age:18})
对比
1
2
3
4
5
6
7
8
1 ref用于基本类型和对象类型
reactive用于对象类型
2 使用ref的值要加.value
reacive的值整个赋值后会失去响应式obj1=obj2,也不能重新reactive,obj1=reactive(obj2),可以使用Object.assign(obj1,obj2)替换

但是ref整个赋值不会失去响应式 obj1.value=obj2

建议都用ref不会失去响应式,当数据很复杂,.value太多时就舍弃ref
toRefs
1
2
3
4
5
6
7
8
9
用于解构赋值,解构出来的是不具备响应式的
let obj= reactive({name:'lxc',age:18})
let {name,age} = obj
这时修改obj.name是响应式的,而修改name不是响应式的

改成let {name,age} = toRefs(obj),此时修改name,obj.name也会跟着变

还有一种不常用 let name =toRef(obj,'name'),let age = toRef(obj,'age')
在非语法糖写法中也会用于return里
computed
1
2
3
4
5
6
7
8
9
10
11
12
13
let fullName =computed(()=>{return Fname+LName })
计算属性是有缓存的,多处使用只会计算一次,方法则没有缓存,每次调用都会执行一次
计算属性是只读的,本质是一个ref响应数据对象

要改成可读写的
let fullName =computed({
get(){ return FName.value + '-' + LName.value },
set(val){
const [str1,str2]=val.split('-')
FName.value = str1,LName.value=str2}
})
const changeFullName=(val: string)=>{fullName.value=val}
由于上边打印发现计算属性本身就是ref
watch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
监视数据,去执行某些操作
和V2作用一样,但只能监视4种数据
ref定义的数据、reactive定义的数据、函数返回一个值(getter函数)、包含上述内容的数组

情况一 监视ref基本类型
基本用法:watch(val,(newVal,oldVal)=>{})
监视的val是ref类型,不需要加.value

如果要结束监视,可以给监视定义一个名字,再调用一次
const stopWatch=watch(val,(newVal,oldVal)=>{ if(oldVal>newVal){stopWatch()} })

情况二 监视ref对象类型
监视的是对象的地址值(整个对象),只改内部属性不会触发watch,若想监视对象内部属性,需要开启深度监视
{deep:true,immediate:true}立即监视会在页面初始化时触发一次watch

虽然只改内部属性能监视到了,但是newVal和oldVal却是一样的
这是因为watch在触发时,会取内存中的新旧地址值,你只改内部属性,这个地址值不会变化,你改整个对象,内存中会创建新的对象地址值

情况三 监视reactive对象类型
默认是开启深度监听的,且deep是关不掉的
改整个reactive用assign本质上也是逐一该内部属性,因此对象地址值是不变的,新旧值是一样的

情况四 只监视对象类型中的某个属性
若该属性是基本类型,必须写成getter函数形式,watch(()=>{return data.name},val=>{}))
若该属性是对象类型,可以直接写(对象类型内的属性本身被封装过,每个属性本身也是reactive类型),但是建议写成函数式,watch(data.obj,val=>{})

如果data.obj直接写会存在问题,改data.obj.a时能监听到,当整个data.obj直接赋值时却监听不到,这是因为data.obj是reactive类型,直接赋值会改变地址值失去响应式监听不到,要用assign
如果写成函数式,注意开启deep:true,写成data.obj不用写deep默认是开启的

情况五 监听以上多个数据
用数组来包含,此时的newVal就是整个数组

总结:1ref基本、2ref对象加deep、3reactive对象自带deep、4对象某属性-函数式加deep或对象式不要直接赋值、5多个数据用数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <van-field v-model="watchBase" type="number" label="监听基本类型" />

<van-field v-text="watchObj.cars" label="监听对象类型" />
<van-button size="small" type="primary" @click="changeC1">改变c1</van-button>
<van-button size="small" type="primary" @click="changeCars">改变cars</van-button>

<script setup lang="ts">
//情况一
const watchBase= ref(0)
const stopWatch1=watch(watchBase,(val)=>{
if(val==10) stopWatch1()
console.log("WatchBase触发")
})
//情况四
const watchObj=ref({name:'张三',cars:{c1:'保时捷',c2:'兰博基尼'}})
const stopWatch2=watch(watchObj.value.cars,(val)=>{
console.log("WatchObj触发")
})
const changeC1=()=>{
watchObj.value.cars.c1="玛莎拉蒂"
}
const changeCars=()=>{
Object.assign(watchObj.value.cars,{c1:'宝马',c2:'奔驰'}) //函数式可以watchObj.value.cars = {c1:'宝马',c2:'奔驰'},watch里监听()=>watchObj.value.cars
}
</script>
watchEffect
1
2
3
4
5
6
7
8
9
立即执行一个函数,并响应式的追踪依赖,依赖变化时就会再次执行
watch需要明确监听参数
watchEffect不需要明确去监听,函数中用到哪个属性就会自动监听

watch([name,age],(val)=>{
let {name,age}=val
if(name=""||age==0){alert(1)} })

watchEffect(()=>{ if(name=""||age==0){alert(1)} })
ref
1
2
3
4
5
6
7
8
9
10
11
12
13
V2写法
<div ref="aaa">12345</div>
this.$refs.aaa

V3写法
<div ref="aaa">12345</div>
let aaa=ref()
此时aaa.value就是 <div>12345</div>

父子组件有同名id会冲突,父子组件有同名的ref不会冲突
ref属性放在组件标签上,拿到的是组件实例,但V2可以随便获取子组件属性,V3只能拿子组件暴露的属性

子组件定义defineExpose({data1,data2,function1})
TS
1
2
3
4
5
6
7
8
9
10
11
接口、自定义类型、泛型
export interface Person { name: string; age?: number; } //?:表示可有可无
export type PersonList = Person[];

import { type Person } from "@/types";

let user: Person = { name: "张三", age: 20 };
let group:Array<Person>=[{ name: "张三"},{ name: "李四", age: 20 }]
let list=reactive<Person[]>([{ name: "张三"},{ name: "李四", age: 20}])

这里Array<Person>和Person[]是一样的,可以给reactive传泛型
Props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
父组件传参 <Son ref="son" :list ="list" />

子组件只接收,js里不能用,template里能用
defineProps(['list'])

子组件接收并且保存起来
let data = defineProps(['list'])

只接收不限制类型,容易出错,可以传泛型
let data = defineProps<{list:PersonList,group:PersonList}>()

接收+限制类型+必要性+默认值
let data =withDefaults(defineProps<{list?:PersonList,group?:PersonList}>(),{list:()=>[],group:()=>[ {name: "张三", age: 20} ]})
其中?:表示可以不传,withDefaults第一个参数和上边一样,第二个参数就是默认值,不过要用函数返回值

对比V2写法
props: {
name: {
type: String,
default: "hello",
required: true
},
list:{
type:Array,
default: ()=>[],//这里的默认值也是函数返回值
required: false
}
},
生命周期
1
2
3
4
5
6
7
8
setup()
onBeforeMount(()=>{})
onMounted(()=>{})
onBeforeUpdate(()=>{})
onUpdated(()=>{})
onBeforeUnmount(()=>{})
onUnmounted(()=>{})
创建、挂载、更新、卸载
Hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
命名规范,use+XXX,本质是一个js,把组合式api的相关数据、方法写在一个文件里,再引入
import useDog from "@/hooks/useDog";

在useDog.js中
export default function(){
let dog="崽崽"
let getDog=()=>{
return "http://dog.com/dog.png"
}
onMounted(()=>{getDog()})
let dogInfo=computed(()=>{return dog+getDog()}) //hook里可以写生命周期和computed
return{dog,getDog,dogInfo} //把数据、方法返回
}
路由-基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
App.vue中
<template>
<RouterLink to="/care" active-class="active">关于</RouterLink>
<RouterLink to="/search" active-class="active">首页</RouterLink>

<RouterView />
</template>

<script setup lang="ts">
import { RouterView } from "vue-router";
</script>

main.ts中
import router from './router'
app.use(router)

router.ts中
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: "/about",
name: "about",
component: () => import("../views/About.vue"),
},
]
})
export default router
路由-进阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
1 工作模式
history: createWebHashHistory() 哈希路由
history: createWebHistory() 历史模式

to的两种写法
<RouterLink to="/care" active-class="active">关于</RouterLink>这种也可写成模板字符串 :to=" `/path?id={{id}}` "
<RouterLink :to="{path:'/care'}" active-class="active">关于</RouterLink>

2 命名路由
当路由有name时,可以通过name跳转
{
path: '/index',
name: 'home',
component: HomeView
},
<RouterLink :to="{name:'/home'}" active-class="active">关于</RouterLink>

3 嵌套路由
{
path: '/index',
name: 'home',
component: HomeView
children:[
{path:'detail',component:Detail}
]
},
<RouterLink to="/home/detail'}">详情</RouterLink>

4 路由传参query
字符串 <RouterLink to="/home/detail?id=99'}">详情</RouterLink>
对象写法 <RouterLink :to="{path:'/home',query:{id:uid}}'}">详情</RouterLink>

route是路由,用来拿当前路由里的参数 this.$route.path
router是路由管理器,用来执行一些跳转操作this.$rouer.push('/home')
V3里写成了hooks,使用useRoute来获取传参

const route = useRoute()
const {query}= toRefs(route) 解构赋值不加toRefs会失去响应式

5 路由传参params
字符串<RouterLink to="/home/detail/张三/99'}">详情</RouterLink>
要在路由里定义接收的参数名(下边对象写法一样)
{
path: '/home',,
component: HomeView
children:[
{name:'detail',path:'detail/:name/:age',component:Detail} //这里定义接收的参数名
]
},

对象写法<RouterLink :to="{name:'detail',params:{id:uid}}'}">详情</RouterLink>
坑:1对象写法不能用path只能用name 2对象写法params里的参数不能是对象和数组,如id:[1,2,3],(即path:'/home/:data',data是对象或数组,跳转时页面刷新会丢失数据) 3如果路由里有接收参数,但是没传会报错,需要加问号
path:'detail/:name?/:age?

6 props配置
{
path: '/home',,
component: HomeView
children:[
{name:'detail',path:'detail/:name/:age',component:Detail,props:true} /
]
},
第一种写法,添加props:true,会将接收的params以props传给Detail组件,<Detail :name="name",:age="age" />
此时可以用props接收参数并使用
defineProps([name,age])

第二种-函数写法,添加props(val){return{x:100,y:200}},
val就是route,包含所有参数
props(route){return{x:route.query.name,y:route.params.id+1}}
可以把任意参数用props传给组件,并且进行加工处理

第三种-对象写法,props:{x:100,y:200}但这种拿不到route参数,没啥用

区别
query 传参配置的是 path,而 params 传参配置的是name,且在 params中配置 path 无效
query传递的参数会显示在地址栏中,而params传参不会
query传参刷新页面数据不会消失,而params传参刷新页面数据会消失
params可以使用动态传参,动态传参的数据会显示在地址栏中,且刷新页面不会消失,因此可以使用动态params传参,根据动态传递参数在传递页面获取数据,以防页面刷新数据消失(即path:'/home/:id')


7 replace跳转
<RouterLink replace to="/home}">首页</RouterLink>
默认页面跳转是push方式,添加replace后就不能后退页面了

8 编程式导航
const router= useRouter()
router.push('/home')
router.replace({name:'detail',params:{}})

9 路由重定向
{
path: '/',,
redirect:'/home'
},

let {data:{id}}={data:{id:10,age:20},name:’’}嵌套解构赋值获取id

pinia-基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
1 在main.js中引入pinia
import { createPinia } from 'pinia'
app.use(createPinia())

2 在store下创建counter.ts存储数据
import { defineStore } from 'pinia'
写法一 带返回值的函数()=>{ return{} },注意数据必须是响应式类型
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
写法二-对象写法{state:()=>(),actions:{}},注意state必须是带返回值的函数,并且默认会是ref类型的数据
export const useCounterStore = defineStore('counter', {
state(){ //这里state只能是函数,不能是state:{ count:0 },也可以写成state:()=>({count:0})
return{ count:0,name:'张三'}
},
actions:{
increment(val) { this.count+=val }//这里边读不到state,但提供了this就是counterStore,注意写成箭头函数形式不能用this
}
})

3 在组件中使用数据
import {useCounterStore} from "@/store/counter.ts"
const counterStore = useCounterStore()
console.log(counterStore)
这里得到的counterStore是reactive类型,打印发现,counterStore里有count和$state属性,$state里也有count
用上边函数写法,得到的count是ref类型

对于let data = reactive({a:1,b:ref(0)}),外层data是reactive时,使用b的值时不用.value,直接使用data.b即可
同理,使用counterStore.count或者counterStore.$state.count都可以

4 修改数据的三种方式
1 在组件里直接改,pinia里count是ref响应式的因此是可以的,但vuex里不能直接改,必须通过action里的方法
counterStore.count=20
2 在组件里批量改
counterStore.$patch({count:20,name:'李四'}) 和一个一个改相比时间线里只执行一次patch操作
3 调用actions里的方法
counterStore.increment(1),打印发现store没有$actions
pinia-进阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1 storeToRefs
const counterStore = useCounterStore()
拿到store后,要使用里边的数据和方法,如果要解构出来更清晰

let {count,increment} = counterStore数据会失去响应式
let {count,increment} = toRefs(counterStore)可以,但会把所有数据和方法都变成ref类型,而方法是不需要变成ref类型的,占用大量内存
let {count} = storeToRefs(counterStore)只关注数据,storeToRefs(counterStore)里只有数据,没有方法

2 getters
可以把state里的数据过滤处理一下
export const useCounterStore = defineStore('counter', () => {
state(){ return{ count:0,name:'张三'}},
getters:{
newCount1(state){
return state.count*10
},
newCount2:state=>state.count*10,//普通函数形式才能用this,this就是counterStore,箭头函数形式不能用this
newCount3():number{return this.count*10} //不接收state时要注明返回类型,如number
}
})

3 $subscribe
可以监听store里数据变化,(适合做持久化处理)
counterStore.$subscribe((mutate, state) => {
localStorage.setItem('count', JSON.stringify(state.count));
})

4 组合式写法
就是最上边第一种写法,里边不能用this,getters可以替换成computed计算属性

注意点1:对象写法中,无论是actions还是getters中,普通函数才能用this,写成箭头函数不能用this
newCount(state){return this.count+1 }这种可以用this
newCount:state=>this.count+1 这种this会报错未定义
组件通信

props、emit 、mitt、v-model、$attrs、$refs+$parent、provide 、vuex+pinia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
1 props
父传子, 父 :data="data" 子 deFineProps(['data'])

子传父,父给子传一个方法,子接收并调用这个方法
父 定义方法getSon(val) 并传给子:test="getSon"
子 接收方法deFineProps(['test']) 并调用test(val)

@click="test(a,$event)"可以传事件和参数

2 emit可以实现子传父
父组件中给子组件绑定自定义事件 <Child @test="test" />
子组件中 const emit = defineEmits(['test'])
emit('test') //触发事件,如果要传参,emit('test',data)
自定义事件别用大写命名,可以用get-data

3 mitt任意组件传值
接收者-提前绑定好事件(订阅消息),发布者-在合适的时候发布消息, pubsub $bus mitt都是相同原理
npm i mitt
import mitt from "mitt";
export default mitt() //生成emitter,创建事件总线

接收数据的组件 import emitter from "@/utils/emitter"
emitter.on('test',(val)=>{count.value=val)}) //绑定事件,把接收的val保存

发送数据的组件 import emitter from "@/utils/emitter"
emitter.emit('test',99)}) //触发事件并传参

onUnmounted()中要解绑事件emitter.off('test')

4 v-model
<input v-model="username"/>
本质是<input type="number" :value="count" @input="count = (<HTMLInputElement>$event.target).value"> //ts可能报错,用了断言
input的v-model底层是动态的value +input事件

<el-input v-model="username" />
本质是<el-input :modelValue="username" @update:modelValue="username=$event" /> **

封装的el-input组件是:
<template>
<input :value="modelValue" @input="emit('update:modelValue',$event.target.value)"> **
</template>
<script setup lang="ts">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

vue2中v-model本质是value传参 + input事件
vue3中v-model本质是modelValue传参 + update:modelValue事件

$event
对于原生事件,$event就是事件对象,能.target取值
对于自定义事件,$event是触发事件时传递的数据,不能.target

v-model是双向数据,既是父传子,也是子传父
v-model的modelValue可以更换,这样是可以实现传多个v-model
<el-input v-model:aaa="username" v-model:bbb="password"/>
等价于<el-input :aaa="username" @update:aaa="username=$event" :bbb="password" @update:bbb="password=$event"/>

5 $attrs
常用于爷传孙:当前组件的父--传给--当前组件的子
父给子传参,会存到子的$attrs,如果子用props接收,就会从$attrs里取出来
父组件<Son :a="0" v-bind="{b:10,c:20}"></Son> //v-bind传对象等价于分开写
子如果props只接收b,则$attrs会变成{a:0,c:20}

子组件如果一个都不接收,子组件中用v-bind="$attrs"传给子的子,就会从爷爷传给孙子
如果孙子要传给爷爷,可以通过$attrs传方法

6 $refs父传子 $parent子传父
V3里写法
父 const son1 =ref() const son2=ref()
子1 deineExpose([data1,func2])
子2 deineExpose([data1,func2])

用$refs能拿到所有子实例,$refs是响应式对象,不用.value
<button @click="test($refs)">点击</button>
test(refs:{[key:string}]:any}){
console.log(refs.son1.data1)
}
用$parent能拿到父实例,但父也需要暴露数据deineExpose

7 provide向后代注入数据,子孙都能用
父传递
let money =ref(100)
provide("qian",money) //不要.value,会失去响应式

子接收
let qian = inject('qian')
当ts推断不好出现类型未定义报错时,可以指定默认值let qian = inject('qian',0)

let money =ref(100) let changeMoney =(val)=>{}
provide("qian",{money,changeMoney})
let {money,changeMoney} = inject("qian",{money:0,changeMoney:val:string=>{}})
可以传方法,实现子传父

justify-content:space-evenly;均分

插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
默认、具名、作用域
当有多个相同的子组件,他们仅有一小部分差别时,分别写成多个组件,再用v-if判断展示是不太好的
我之前常用的写法是,在这个组件里把所有差别都写了,当用组件时传个值,根据传的值判断用哪个差别

插槽的做法是:定义组件时,把有差别的区域空出来,用组件时把这个差别传过去,让他展示到空出来的区域

默认插槽
<AAA> <h1>你好</h1> </AAA> 在自定义组件里写标签,页面不会展示(vue不知道把和h1插入到AAA里的哪个位置)
在AAA里写<slot /> 这时写的标签就会展示到slot的位置

定义AAA
<template>
<div>下边展示不同内容</div>
<slot />
<div>展示完了</div>
</template>

使用AAA
<AAA> <h1>我是slot要展示的</h1> </AAA>

具名插槽
可以插入多个内容

定义AAA
<template>
<div>下边展示不同内容</div>
<slot name='slot1'/>
<div>展示完了</div>
<slot name='slot2'/>
</template>

使用AAA,v-slot只能用到组件或template标签上
<AAA >
<template v-slot="slot1">我是slot1</template>
<template v-slot="slot2">我是slot2</template>
</AAA>

<AAA v-slot="slot1"> //它只会展示到slot1位置
<h1>我是slot要展示的</h1>
</AAA>

简写方式v-slot="slot1"可以写成 #slot1
默认插槽其实也有名字,是"default"

作用域插槽
当使用插槽时,<template #slot1>{{data}}</template> 里要用AAA里的数据data
可以把slot当成AAA的子组件给他传参,使用AAA时的v-slot="params"的params里会接收所有参数

默认插槽+作用域插槽
定义AAA
<template>
<div>下边展示不同内容</div>
<slot :data="data" x=100 />
<div>展示完了</div>
</template>
使用AAA
<AAA v-slot="params">
<h1>{{params.data}}</h1>
</AAA>

具名插槽+作用域插槽:只需要把 v-slot="params" 改成 v-slot:slot1="params",简写方式#slot1="params" (v-slot="params"等价于#default="params")
定义AAA
<template>
<div>下边展示不同内容</div>
<slot name='slot1' :data="data" x=100 />
<div>展示完了</div>
<slot name='slot2' :data="data" y=99/>
</template>
使用AAA
<AAA >
<template v-slot:slot1="params">{{params.data}}</template>
<template v-slot:slot2="params">{{params.y}}</template> //slot1和slot2的params属于不同作用域,值不一样
</AAA>

总结作用域插槽-数据在子组件AAA里,但AAA的子组件slot的样式却在AAA的父组件这里
其他Api
shallowRef + shallowReactive
1
2
3
4
5
6
7
8
9
10
11
12
浅层的ref和reactive
当改整个对象是响应式的,当改对象内部属性时不管用

let data =shallowRef({name:'张三',18})

data.value = {{name:'李四',18} 可以改成功
data.value.name = '王五' 改不了,(打印发现name的值变了,只是页面没有变,而且data内部的属性name不是reactive类型,之前使用ref内部属性都是reactive类型)

let data = shallowReactive({name:'张三',info:{age:19}})
对于shallowReactive,改name和整个info可以成功,改info里的age不是响应式

作用:只关注和处理第一层,节省内存 (注意深层的值会变,但不是响应式,页面不会跟着变)
readonly +shalowReadonly
1
2
3
4
5
6
7
8
readonly的参数必须是响应式类型

let sum1 =ref(0)
let sum2 = readonly(sum1) //不能写成readonly(sum.value)
sum2不能修改(js里不能改,页面输入框改了,它js里打印值还是不变的),但sum1改的时候,sum2还会跟着sum1变

shalowReadonly 浅层的不可修改,只有第一层不能改(节省内存),改深层能改
readonly 深层的不可修改(给深层每个属性都加了readonly)
toRaw + markRaw
1
2
3
4
5
6
7
8
9
10
11
12
13
toRaw用来获取响应式数据的原始数据
let data =reactive({name:'张三',18}) 打印data是一个Proxy对象
let data2 =toRaw(data) 打印data2就是纯数据{name:'张三',18}

用途:把响应式变成非响应式的
场景1-我页面只展示过滤器加工后的数据不修改,可以用过滤器,也可以展示toRaw再加工后的数据,而实际的值不会变
场景2-把数据发给后端,或者传给第三方库如lodash处理,但不想页面跟着变

markRow用来标记数据,使其永远不会变成响应式的
let data = markRaw({name:'张三',18})
let data2 =reactive(data)//此时不管用,打印data2还是原始数据

场景:使用第三方库如mockjs时,防止把它们暴露的对象变成响应式的
customRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
原始的ref,不能实现某些特别需求:当修改后,1秒后页面再变化
let initValue ="hello"
let msg = ref(initValue)
customRef是自定义的ref,ref的本质是
let msg = customRef((track,trigger)=>{
return {
get(){
track() //告诉Vue这个msg很重要,需要进行持续关注,一旦变化就去更新
return initValue
},
set(value){
initValue = value
trigger() //通知Vue一下数据msg变化了
}
}
})

let timer:number //定时器返回的是编号数字
set(value){
clearTimeout(timer)
timer=setTimeout(()=>{
initValue = value
trigger()
},1000)
}

企业开发时常把自定义ref封装成hooks
export default function(initValue,delay){
XXXX //这里写自定义ref的逻辑,甚至可以调接口
return{msg}
}
使用时let msg= useMsgRef(initValue,delay)
teleport传送
1
2
3
4
5
6
7
8
9
父组件里有个子组件弹窗,让这个弹窗以窗口来定位,可以用position:fixed,但是有些样式会影响fixed,如
css样式 filter:saturate(0%) 滤镜:饱和度0,可以把页面置灰
当给父组件添加filter样式后,子弹窗不再以body来定位,而是以父组件来定位

<teleport to="body">
<div>xxx</div>
</teleport>

可以把div结构塞进任意想要的位置,to=""里是标签选择器
Suspense异步组件
1
2
3
4
5
6
7
8
9
10
11
12
父组件中有个子组件,子组件用了异步请求,但在setup语法糖写法中没办法添加async
let {res:{data}} = await axios.get(url) //解构出res中的data

为了解决这种问题,可以给组件添加顶层async,Suspense底层是用插槽实现的(这个功能还在试验中)
<Suspense>
<template #default> //请求成功时
<Child />
</template>
<template #fallback> //请求未成功时显示的内容
<div>加载中</div>
</template>
</Suspense>
全局api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.component('hello',Hello) 可以把Hello注册为全局组件,到处都能用

app.config.globalProperties.x=10 可以挂载全局属性,到处都能用这个参数,vue2中的做法是Vue.prototype.x=10
但对于ts会报错,还需要添加定义
declare module 'vue' {
interface ComponentCustomProperties{
x:number
}
}

app.directive('beauty',(element,{value})=>{ 自定义指令,使用时用v-beauty
element.innerText += value //字体大小
element.style.color="red"
})
<div v-beauty="count">开心</div>

app.mount('#app') 挂载应用
app.unmount('#app') 卸载应用
app.use() 安装插件,vue2中是Vue.use()
其他改变
1
2
3
4
5
6
7
8
9
10
Vue3文档-——从Vue2迁移——非兼容性改变

1 过渡类名v-enter变为v-enter-from,v-leave变为v-leave-from
2 v-on不再支持keyCode,如@keyup.13 对应13的键盘按钮
3 v-model包含并替换掉了v-bind.sync 如a.sync="sum"
4 v-if和v-for在vue2中不能同时用且v-for优先级更高,vue3中可以同时用,且v-if优先级更高
5 移除了$on、$off、$once
6 移除了过滤器 filter,可以用computed和自定义方法替代
7 移除了$children实例propert
。。。更多的参见官方文档

知识点概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ref和reactive
toRefs
computed-普通的和可读写的
watch-4种
watchEffect
ref-defineExpose
deineProps
生命周期
router-传参query/params-接收useRoute/props-router
pinia
组件通信-props\emit\mitt\v-model\$attrs\$refs\provide
插槽
shallowRef\readonly\toRaw\customeRef\teleport
suspense