基础知识

ref和reactive

ref可以定义基本数据类型和对象类型,用ref定义对象类型数据时,底层使用的其实就是reactive

reactive只能定义对象类型数据,reactive定义的对象不能直接赋值修改整个对象,否则会失去响应式

1
2
3
4
const obj = reactive({ name: 'zs' })
obj.name = "ls" // obj保持响应式
obj = { name: 'ls' } // obj响应式丢失
Object.assign(obj,{ name: 'ls' }) // obj保持响应式

toRefs和toRef

作用:从响应式对象解构时,将数据转换为响应式的

toRefs:解构时,批量将对象属性全转换为ref响应式对象

toRef:解构时,将对象中指定属性转换为ref响应式对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = reactive({
name: 'zs',
age: 15,
sex: 'man'
})
// 解构对象后name和age丢失响应式
const { name, age } = obj

// 使用toRefs
// name和age都是响应式,直接修改name,页面不管调用的是obj.name或者name都会发生变化
const { name,age } = toRefs(obj)

// 使用toRef
// 直接修改oSex将同时修改obj.sex
const oSex = toRef(obj, 'sex')

计算属性computed

  • 计算属性有缓存

  • 依赖的数据变化,计算结果跟着变化

  • 页面不管调用几次,同一计算属性值,渲染过程中只计算一次(若是方法形式实现数据计算,即是页面调用几次,将计算几次)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const name = "zs"

// 只读的情况
const ccucc = computed(()=>{
return name + '123'
})

// 可读可写,根据业务情况自动判断是get还是set
const ccucc = computed({
get(){
return name + '123'
},
set(val){
name.value = val
}
})
console.log(ccucc.value) // 将自动走计算属性中的get, ccucc.value输出为zs123
ccucc.value = 'ls' // 将自动走计算属性中的set, ccucc.value输出修改为ls123

监听watch

watch能监听以下四种数据:

  • ref定义的数据
  • reactive定义的数据
  • 函数返回一个值(getter函数)
  • 一个包含上述内容的数组

ref定义的基本数据类型

基本使用

1
2
3
4
5
6
7
const name = ref("zs")

// watch监听ref时,不需要 .value ,watch监听的ref定义的数据name,而不是数据本身'zs'
watch(name, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})
1
2
3
4
// 只关注新值
watch(name, (val) => {
console('新值:' + val)
})

停止监听

1
2
3
4
5
6
7
8
// 当值修改为'ls'后,watch将不再监听数据的变化
const stopWatch = watch(name, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
if(newVal === 'ls'){
stopWatch()
}
})

ref定义的对象数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = ref({
name: 'zs',
age: 18
})

// 监听obj对象地址,若仅修改某个属性将不触发watch,只有重新给obj赋值才触发
watch(obj, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})
// 开启深度监测后,修改属性也会触发watch
watch(obj, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
},{
deep:true
})

新旧值出现相同原因

情况一:在修改某个属性时,新旧值返回一样。原因是对象地址没有变化,新值和旧值读取的都是一个值

即:obj.namezs 变成了ls,watch监听到变化,新旧值都是从同一个内存地址的对象obj.name拿取,所以返回的一样的值

情况二:若是修改整个对象,那么新旧值读取的将是两个不同的内存地址,所以返回的值是不一样,

即:oldVal拿取的是修改前内存地址的对象,newVal拿取的是现在修改后指向的内存地址值

reactive定义的数据监听

1
2
3
4
5
6
7
8
9
10
11
const obj = reactive({
name: 'zs',
age: 18
})

// reactive默认开启深度监测,即修改属性和对象都会触发watch
watch(obj, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})

新旧值出现相同原因同上ref

reactive默认开启的深度监听,原来版本不支持手动关闭,但是在最新版支持手动关闭?未测试

监听对象中的某个属性

  • 当只监听对象中某个基础数据类型属性时,需要以getter函数的形式完成监听某个值(即,一个返回值函数)
1
2
3
4
5
6
7
8
9
10
11
12
const obj = reactive({
name: 'zs',
age: 18,
sp: {
type: 'XL'
}
})

watch(()=> obj.name, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})
  • 当只监听对象中某个对象数据类型属性时,可以直接监听,也可写成函数返回值形式,但两者略有不同。
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
const obj = reactive({
name: 'zs',
age: 18,
sp: {
type: 'XL',
money: 666
}
})

// 直接监听时,修改其中任意一个,都会触发,但是修改整个sp属性时不触发
watch(obj.sp, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})
// 函数返回形式监听时,修改其中任意一个,不会触发,但是修改整个sp属性时会触发
// 此时监听的是地址值
watch(() => obj.sp, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
})
// 综合以上,采用第二种写法,加上深度监听,不管修改某个还是整个sp属性,都触发
watch(() => obj.sp, (newVal,oldVal) => {
console('新值:' + newVal)
console('旧值:' + oldVal)
},{
deep:true
})

监听多个不同数据

基于上述情况,按需求放到数组中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = reactive({
name: 'zs',
age: 18,
sp: {
type: 'XL',
money: 666
}
})
// 单个监听规则同上
watch([() => obj.name,() => obj.sp.type], (newVal,oldVal) => {
console('新值:' + newVal) // 新值数组 ...
console('旧值:' + oldVal) // 旧值数组 ['zs','XL'],与监听时的顺序对应
},{
deep:true
})

watchEffect

官方:立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行改函数

watchwatchEffect对比

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
  2. watch:要明确指出监听的数据
  3. watchEffect:不用明确指出监听的数据(函数中用到哪些数据,那就监听哪些数据)
1
2
3
4
5
6
7
8
9
10
const a = ref(0)
const b = ref(55)

// 回调函数中用到即监听,所以面向结果
// 当要监听的多个不同数据间的变化结果情况,很方便
watchEffect(()=>{
if(a > 99){
console.log("a 大于 99 了")
}
})

标签ref属性

  • html标签上,返回当前元素
1
2
3
4
5
6
<div ref="ccucc"></div>

<script>
// 创建一个ccucc,存储ref标记的内容
let ccucc = ref()
</script>
  • 在一个定义的组件上,返回的是当前组件实例对象
1
2
3
4
5
6
<MyCard ref="ccucc"></MyCard>

<script>
// 配合子组件中的defineExpose API 可以拿到子组件中的数据
let ccucc = ref()
</script>

defineProps

接收父组件传值

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
/** 
* 父组件
*/
<MyCard a='zs' :b='list'></MyCard>

const list = reactive({
name: 'zs',
age: 15
})

/**
* 子组件接受的情形
*/

// 1. 直接读取,只接收数据
defineProps(['a','list'])

// 2. 接收数据 + 限制数据类型
interface listModel {
name: string,
age: number
}
defineProps<{ list:listModel, a:string }>()

// 3. 接收数据 + 限制数据类型 + 是否必传
interface listModel {
name: string,
age: number
}
defineProps<{ list:listModel, a?:string }>()

// 4. 接收数据 + 限制数据类型 + 是否必传 + 指定默认值
import { withDefaults } from "vue"

interface listModel {
name: string,
age: number
}
withDefaults(defineProps<{ list: listModel; a?: string }>(), {
// 复杂数据类型需要写成函数返回值形式(下面注意箭头函数返回值写法,直接返回{}对象需要小括号包裹,区别于代码块)
list: () => ({
name: "ls",
age: 18
}),
a: "默认值"
});

组件生命周期

总的概括为:创建、挂载、更新、销毁

  • Vue2

beforeCreate(创建前)、created(创建完毕)

beforeMount(挂载前)、mounted(挂载完毕)

beforeUpdate(更新前)、update(更新完毕)

beforeDestroy(销毁前)、destroy(销毁完毕)

  • Vue3

在setup中就已经完成创建

onBeforeMount(挂载前)、onMounted(挂载完毕)

onBeforeUpdate(更新前)、onUpdated(更新完毕)

onBeforeUnmount(销毁前)、onUnmounted(销毁完毕)

  • 常用钩子:onMountedonUpdatedonBeforeUnmount

  • 相关注意事项

  1. 渲染阶段子组件优先于父组件

  2. v-if 可以控制组件的销毁

自定义Hooks

本质:同一功能或相关功能和数据的抽离,组合式API的体现

优点:功能清晰易维护功能,各个hooks中都可调用生命周期钩子

常用命名:通常use[Name].[ts/js]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** useSum.ts */
export default function(){
const c = 55
const sum = (a:number,b:number) =>{
return a + b + c
}

return {
sum
}
}

/** 其余页面调用 */
import useSum from './useSum'

const { sum } = useSum()

console.log(sum(3,5))

组件通信

props

使用频率最高,父<=>

  • 若父 => 子:属性值是非函数
  • 若父 <= 子:属性值是函数

father.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<Child :name="name" :sendAge = "getAge"/>
</template>
<script setup lang="ts">

let name = ref('zs')

let age = ref()

// 儿子触发函数,拿到儿子的年龄
const getAge = (val:number) => {
age.value = val
}

</script>

child.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
{{ name }}
<button @click="sendAge(age)">把年龄给父亲</button>
</div>
</template>
<script setup lang="ts">
let age = ref(99)

// 声明接收props
defineProps(['name','sendAge'])
</script>

自定义事件

<=

  • 自定义事件时命名官方推荐keybab-case的事件命名,非驼峰

father.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<Child @test-age="getAge(age)">把年龄给父亲</Child>
</template>
<script setup lang="ts">

let age = ref()

// 儿子触发函数,拿到儿子的年龄
const getAge = (val:number) => {
age.value = val
}

</script>

child.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
{{ name }}
<button @click="sendAge">把年龄给父亲</button>
</div>
</template>
<script setup lang="ts">

let age = ref(99)

// 声明事件
const emit = defineEmits(['test-age'])

const sendAge = () => {
// 触发事件
emit("test-age", age.value)
}
</script>

mitt

任意组件通信

  • 安装
1
npm i mitt
  • 配置

src\utils\emitter.ts

1
2
3
4
import mitt from 'mitt'
// 调用mitt得到emitter,emitter可以绑定事件、触发事件
const emitter = mitt()
export default emitter

main.ts

1
import emitter from '@/utils/emitter'
  • 使用
1
2
3
4
5
6
7
8
9
10
import emitter from '@/utils/emitter'
// 绑定事件
emitter.on('test',()=>{
...
})
// 触发事件
emitter.emit('test')
// 解绑事件
emitter.off('test')
emitter.all.clear()
  • 应用

father.vue

1
2
3
4
<template>
<Child1 @test-age="getAge(age)">把年龄给父亲</Child1>
<Child2 @test-age="getAge(age)">把年龄给父亲</Child2>
</template>

child1.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
收到child2的年龄 {{ age }}
</div>
</template>
<script setup lang="ts">
import emitter from '@/utils/emitter'

let age = ref()
// emitter绑定send-age事件,只要事件触发就会接收数据
emitter.on('send-age',(val:number)=>{
age.value = val
})

// 组件卸载时解绑事件
onUnmounted(() => {
emitter.off('send-age')
})
</script>

child2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<button @click="toAge">把年龄给兄弟child1</button>
<!-- 直接触发给值 -->
<button @click="emitter.emit('send-age', age)">把年龄给兄弟child1</button>
</div>
</template>
<script setup lang="ts">
import emitter from '@/utils/emitter'
let age = ref(99)

const toAge = () =>{
// 触发,发送数据
emitter.emit('send-age', age.value)
}

</script>

v-model

<=>

father.vue

  • v-model用在html标签上,将实现双向绑定

下面两种写法,第一为v-model,第二为底层实现

第二种写法中:value="name"是页面到数据,@input="name = $event.target.value"是数据到页面,即前者页面数据变则绑定的数据就变后者实现绑定的数据变则页面的数据就变,合着完成的就是双向绑定

1
2
3
4
5
6
7
8
9
<template>
<input type="text" v-model="name" />
<input type="text" :value="name" @input="name = (<HTMLInputElement>$event.target).value" />
</template>
<script setup lang="ts">

let name = ref('zs')

</script>
  • v-model用在组件标签上

father.vue

1
2
3
4
5
<template>
<Child1 v-model="name"></Child1>
<!-- 上面完整写法 -->
<Child1 :modelValue="name" @update:modelValue="$event"></Child1>
</template>

update:modelValue是一个事件名称,类似自定义事件,只是名称带有规范

$event:对于原生事件,$event是事件对象,有target属性。对于自定义事件,$event是触发事件时所传递的数据,无target属性

child1.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<input type="text" :value="modelValue" @input="changeInput" />
</template>
<script setup lang="ts">
defineProps['modelValue']

const emit = defineEmits(['update:modelValue'])

const changeInput = (e)=>{
emit('update:modelValue',e.target.value)
}
</script>

生态相关

路由(vue-router)

基础使用

  • 安装路由 npm i vue-router

  • 创建路由 src/router/index.ts

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
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "HelloWorld",
component: () => import("../components/HelloWorld.vue"),
children: []
},
{
path: "/Cesium",
name: "Cesium",
component: () => import("../components/Cesium.vue")
},
{
path: "/Openlayer",
name: "Openlayer",
component: () => import("../components/Openlayer.vue")
}
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

  • 引入路由main.ts
1
2
3
4
5
6
7
8
import { createApp } from "vue";
// 引入路由
import router from './router'

const app = createApp(App);
// 使用路由
app.use(router)
app.mount("#app");
  • 显示路由
1
2
3
4
5
6
7
<div>  
// active-class可定义当前激活的样式
<router-link to="/home" active-class='active'>首页</router-link>
<router-link :to="{ name: 'about' }" active-class='active'>关于</router-link>
<router-link :to="{ path: '/login' }" active-class='active'>登录页</router-link>
<router-view />
</div>
  1. 上面为路由跳转方式的一种,另外一种函数形式调用useRouter().push()

  2. active-class可以设置激活状态下的样式

注意

  • 区分:路由组件和一般组件

路由组件:网页调用中通过路由来调取到页面中的称为路由组件。一般存放在项目中的views或者pages文件夹下。

一般组件:网页调用中通过标签引入的形式,来调取到页面中的称为一般组件。一般存放在项目中的component文件夹下。

  • 通过点击导航,从页面”消失”的路由组件,默认被卸载,需要的时候再会去挂载。

路由器工作模式

  1. history模式

优点:URL更加美观,不带有#,更接近传统的网站

缺点:服务器需要做路径处理,否则刷新会出现404

1
2
3
const router = createRouter({
history: createWebHistory() // vue3中 history模式
})
1
2
3
4
# nginx 配置处理路径
location / {
try_files $uri $uri/ /index.html;
}
  1. hash模式

优点:兼容性更好,因为不用服务端处理路径。

缺点:URL会带有#不美观,且在SEO优化方面相对较差。

1
2
3
const router = createRouter({
history: createWebHashHistory() // vue3中 hash模式
})
  1. 后台一般采用hash,电商等官网类一般history

路由传参

  • query

1
2
<RouterLink to="/home?id=55">
<RouterLink :to="{ path:'/home', query: { id: 55 }}">

1
2
3
4
import { useRoute } from "vue-router"
const route = useRoute()

console.log(route.query.id)
  • params

1
2
3
4
5
6
7
8
// 路由占位
{
name: "home",
path: "home/:id/:name?"
}
// 传入参数
<RouterLink to="/home/55/zs">
<RouterLink :to="{ name:'home', params: { id: 55, name: 'zs' }}">
  • 采用params传参数时,若是采用对象写法,只能通过name来指定路由

  • 不能传递数组类型

  • 占位中/:name?表示可选参数

  • 若非可选参数没传值,路由将报错

1
2
3
4
5
import { useRoute } from "vue-router"
const route = useRoute()

console.log(route.params.id)
console.log(route.params.name)

路由的props

通过配置路由props参数,结合defineProps实现路由传参

  1. 传递params参数,需要地址占位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: "/",
...
children: [
{
name: "home",
component: () => Home
path: "home/:id/:name?",
props: true
}
]
}
]
1
2
3
4
5
// 跳转
<RouterLink :to="{ name:'home', params: { id: 55, name: 'zs' }}">

// 接收传递的参数
defineProps(["id","name"])
  1. 传递query参数,不需要地址占位,函数式写法返回路由中的query对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: "/",
...
children: [
{
name: "home",
component: () => Home
path: "home",
props(route){
// route就是当前路由信息
return route.query
}
}
]
}
]
1
2
3
4
5
// 跳转
<RouterLink :to="{ name:'home', query: { id: 55, name: 'zs' }}">

// 接收传递的参数
defineProps(["id","name"])
  1. 传递固定参数,对象式写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: "/",
...
children: [
{
name: "home",
component: () => Home
path: "home",
props: {
id: 55
}
}
]
}
]
1
2
3
4
<RouterLink :to="{ name:'home' }">

// 接收传递的参数
defineProps(["id"])

使用路由的props模式传递参数实质是,普通defineProps传递函数的逻辑,vue将自动在路由组件上加上<Home :id="id" :name="name" />,所以子组件用defineProps取值就行。

路由replace

默认路由跳转为push,相当于每跳转一下就会追加一个路由,点击返回就可回到上一次的路由

相反,replace将替换原来路由,所以其无法返回上一个路由

1
<RouterLink replace :to="{ name:'home', query: { id: 55, name: 'zs' }}">

编程式路由导航

通过一个路由的钩子函数进行跳转,其中router.push中的参数写法和前面标签<router-link />中的to参数一样。

1
2
3
4
5
6
7
import { useRouter } from "vue-router"

const router = useRouter()

router.push('/home')

router.replace('/home')

路由重定向

默认项目启动,会读取/路由,若没有该路由地址,控制台会提示警告。一般采取一个重定向将某个路由组件放在/路由下,再通过redirect进行重定向过去。即自动跳转到目标路径下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/home",
},
{
name: "home",
component: () => Home
path: "/home",
props: {
id: 55
}
}
]

Pinia

集中式状态(数据)管理(reduxvuexPinia

基本使用

  1. 安装pinia
1
npm i pinia
  1. 引入pinia

main.ts

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
// 引入pinia
import { createPinia } from 'pinia'
// 创建pinia
const app = createApp(App)
// 安装pinia
app.use(pinia)
app.mount('#app')
  1. 应用

规范

  1. 所有pinia相关内容放在src\store\目录下
  2. 文件命名通常与相关组件同名,如当前文件记录home组件相关数据,那通常命名为home.tsHome.ts
  3. 暴露对象时,通常采取hooks的方式,如useHomeStore

home.ts

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
import { defineStore } from 'pinia'

const useHomeStore = defineStore('Home', {
state: () => {
return {
name: "zs",
age: 15
}
},
getters:{
// 箭头函数,一行代码返回的写法
grow: state => state.sum + 1 ,
// this写法,也可通过state取值
delAge(){
return this.sum - 1
}
},
actions:{
editName(name:string){
// this指向当前useHomeStore
this.name = name
}
}
})

export default useHomeStore

home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{ homeStore.name }}
<!-- getters计算值 -->
{{ homeStore.grow }}
</div>
</template>
<script setup lang="ts">
import { useHomeStore } from "@/store/home"

const homeStore = useHomeStore()
// 读值
console.log(homeStore.name)

</script>
<style scoped lang="scss"></style>
  1. pinia数据修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useHomeStore } from "@/store/home"

const homeStore = useHomeStore()

// 第一种
homeStore.name = "ls"

// 第二种(批量数据修改)
homeStore.$patch({
name: 'ls',
age: 18
})

// 第三种(借助pinia中的actions,通常应用于业务复杂的修改,以及复用量大的情况)
homeStore.editName("ls")

storeToRefs

作用:解构store中数据后,进行响应式转换

区别toRefs:只会关注store中的数据,不对方法起作用,而toRefs会将store中所有东西转换为响应式,会将所有方法进行转换。所以pinia数据响应式转换用storeToRefs

1
2
3
4
5
import { storeToRefs } from 'pinia'
import { useHomeStore } from "@/store/home"

const homeStore = useHomeStore()
const { name, age, grow } = storeToRefs(homeStore)

$subscribe

作用:监听store中数据变化

1
2
3
4
5
6
7
import { useHomeStore } from "@/store/home"

const homeStore = useHomeStore()

homeStore.$subscribe((mutate,state)=>{
console.log('homeStore中数据变化了')
})

组合式写法

home.ts

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
import { defineStore } from 'pinia'
import { reactive, computed } from "vue"

const useHomeStore = defineStore('Home', {
// state
const homeList = reactive({
name: "zs",
age: 15
})
// getters
const grow = computed(()=>{
return homeList.age + 1
})

const editName = (name:string) => {
homeList.name = name
}
// actions
return {
homeList,
grow,
editName
}

})

问题解答

vue3中的setup 能否同vue2中的 data、methods同时存在?

可以同时存在,且setup的生命周期前于data和methods,所以在data中可以通过this访问到setup中定义的数据