一、关于Ref
1.shallowRef()
shallowRef
是 Vue 3 中新引入的响应式数据类型之一,它与 ref 类型非常相似,但是有一些不同点。
不同的是,shallowRef
只会对其包装的对象进行浅层次的响应式处理,即如果这个对象的子属性发生改变,那么这个改变不会被响应到视图中。
<template>
<div>{{ Man }}</div>
<button @click="change">修改</button>
</template>
<script setup lang="ts">
import { shallowRef } from "vue";
const Man = shallowRef({ name: "小4" });
const change = () => {
Man.value.name = "小5";
console.log(Man);
};
</script>
<style lang="scss" scoped></style>
需要注意的是,如果响应函数里有Ref,则shallowRef也会变成深响应式,浅响应式shallowRef
会被深响应式ref
影响。
shallowRef
的应用场景一般是在某些需要保持数据响应式特性的情况下使用,但同时也要避免做出过多的响应式开销。比如:
- 针对复杂的大型数据对象,如果其中只有部分属性需要在组件渲染时被访问或监听,可以考虑使用 shallowRef 来对这些属性进行响应式封装,减少响应式追踪所需的开销。
- 在某些自定义 hook 中,为了确保能够及时地更新相关的状态或数据,可以使用 shallowRef 来跟踪某个对象的属性,当其中某些属性发生变化时,就能立即执行后续的逻辑处理。
- 在与 Vuex 进行状态管理的时候,有些大型对象可能只需要针对部分属性进行响应式处理,此时使用 shallowRef 可以有效减少响应式系统的压力
2.triggerRef()
可以强制更新浅响应式的数据
具体来说,triggerRef 接收一个 ref 对象,并立即触发它的 setter 函数来更新该 ref 的值。这条更新操作会自动触发组件重新渲染,从而达到实时更新视图的效果。
let message: Ref<Obj> = shallowRef({
name: "小5"
})
const changeMsg = () => {
message.value.name = '大5'
// 浅响应变深响应
triggerRef(message)
}
triggerRef
的用途比较灵活,可以在多种场景下使用。例如:
- 某些情况下需要根据用户输入实时更新数据显示,此时可以为相应的
ref
对象设置监听器,在变化时使用triggerRef
强制更新。 - 在某些异步加载数据的情况下,可能需要等待一些时间才能获取到完整的数据。如果依赖于这些数据的组件需要及时更新,也可以将这些关键数据封装进
ref
对象中,在异步任务完成后使用triggerRef
更新数据。
需要注意的是,triggerRef
虽然可以强制触发视图更新,但是不建议频繁使用。过度使用会导致性能问题和代码可读性下降。通常情况下,Vue 的响应式系统会自动检测数据变化并触发更新,只有在必要的情况下才考虑手动触发。
深响应式ref
底层会自动调用triggerRef
,因此要实现浅响应式,不能把深响应式放在一块。
3.customRef()
customRef
用于创造一个自定义的ref
类型。与ref
及其变体如 shallowRef
不同,customRef
允许我们自己决定如何处理设置(ref.value)并得到(ref.value)读取操作。
具体地说,customRef
接收一个工厂函数 (track
, trigger
),该函数需要返回一个对象,该对象包含了 get
和 set
两个方法。这两个方法的作用分别对于 ref.value
的读取和更新操作,可以自行定义它们的实现逻辑。同时,还需要注意在读取或者更新值时手动调用 track
和 trigger
函数,以便进行依赖追踪和触发更新:
import { customRef } from 'vue'
const myCustomRef = customRef((track, trigger) => {
let value = 0
return {
get() {
// 依赖追踪
track()
return value
},
set(newValue) {
// 更新值
value = newValue
// 触发组件重新渲染
trigger()
}
}
})
customRef
的应用场景比较灵活,通常用于以下情况:
- 当需要对一些数据的读取和更新进行特殊的逻辑处理时,例如将数据进行格式化或者验证等,可以使用
customRef
来自定义 ref 的行为。 - 当需要对一些本身不可响应的数据(例如函数或 DOM 元素)进行响应式处理时,可以使用
customRef
来替代ref
和reactive
等原有的响应式 API。
需要注意的是,customRef 是一个相对高级的 API,在开发中建议慎用。同时在对其使用时,也需要关注性能、代码可读性及其使用场景等因素。
二、关于Reactive
1.reactive()
reactive定义的对象是不能直接赋值,否则会破坏响应式对象
import { reactive } from "vue";
let list = reactive<string[]>([]);
const add = () => {
setTimeout(() => {
let res = ["EDG", "RNG", "JDG"];
console.log(list);
list.push(...res); // 正确的修改reactive对象方法
// list = res; // reactive proxy 不能直接赋值,否则会破坏响应式对象
console.log(list);
}, 2000);
};
2.readonly()
把属性变成只读的
const info = ref('xiao5')
const newInfo = readonly(info)
newInfo.value = '555555' // 会报错,提示newInfo是只读属性
3.shallowReactive()
和shallowRef类似。
三、关于to系列
1.toRef()
toRef
用于创建一个针对源对象的响应式引用。它将一个对象的属性转换为单独的 ref,以便更容易地进行响应式处理。
具体来说,toRef 接收一个源对象和一个属性键名作为参数,并返回一个 ref 对象。这个 ref 对象会自动跟踪源对象中对应属性的变化,并在变化时更新视图。
下面是一个使用 toRef 的例子:
import { toRef } from 'vue'
const myObj = {
name: "vue",
version: "3.2"
}
const myRef = toRef(myObj, "version")
上述代码中,我们创建了一个名为 myObj
的对象,并通过 toRef
将其中的 version
属性转换为一个 ref
对象。这样,当我们修改 myObj.version
的值时,myRef.value
的值也会随之更新。
有了 toRef
,我们就可以方便地将源对象中特定的属性进行响应式处理,而不需要将整个源对象都转化为响应式对象或者手动为每个属性创建 ref。
需要注意的是,**toRef 返回的 ref 对象本质上只是一个引用,而非复制。**因此如果在程序中同时存在多个对同一个 ref 对象的引用,那么它们之间的关系和更新状态是相互影响的。
2.toRefs()
toRefs
可以帮我们批量创建ref
对象,主要是方便我们解构reactive
import {reactive, toRefs} from 'vue'
const obj = reactive({
foo: 1,
bar: 1
})
let {foo, bar} = toRefs(obj)
foo.value++;
console.log(foo.value, bar.value); // 2 1
3.toRaw()
将响应式对象转换成普通对象
四、computed计算属性
计算属性就是当该计算属性所依赖的属性的值发生变化的时候,才会触发该计算属性的更改。
1.函数形式
import { computed, reactive, ref } from 'vue'
let price = ref(0)//$0
let m = computed<string>(()=>{
return `$` + price.value
})
price.value = 500
注意:函数形式只能get,不能set
2.对象形式
<template>
<div>{{ mul }}</div>
<div @click="mul = 100">click</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
let price = ref<number | string>(1) //$0
let mul = computed({
get: () => {
return price.value
},
set: (value) => {
price.value = 'set' + value
}
})
</script>
对象形式能get,也能set
3.computed之购物车案例
<template>
<div>
<input type="text" placeholder="搜索" v-model="keyword"/>
<div style="margin-top: 20px">
<table border width="600" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>物品名称</th>
<th>物品单价</th>
<th>物品数量</th>
<th>物品总价</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filterData" :key="item.name">
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td>
<button @click="item.num > 0 ? item.num-- : item.num">-</button
>{{ item.num }} <button @click="item.num++">+</button>
</td>
<td>{{ item.num * item.price }}</td>
<td><button @click="delItem(item)">删除</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5" align="right">总价:{{ sumPrice }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
interface Data {
name: string;
price: number;
num: number;
}
let keyword = ref<string>('')
// 计算属性实现搜索功能
let filterData = computed(() => {
return data.filter(item => {
return item.name.includes(keyword.value)
})
})
let data = reactive<Data[]>([
{
name: "小5的绿毛",
price: 500,
num: 1,
},
{
name: "小5的红衣",
price: 5000,
num: 2,
},
{
name: "小5的黑袜",
price: 500000,
num: 1,
},
]);
// 计算属性总价
let sumPrice = computed<number>(() => {
return data.reduce((preValue:number, item:Data) => {
return preValue + item.num * item.price;
}, 0);
});
const delItem = (item: Data) => {
const index:number = data.findIndex((item1) => item1 == item);
data.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>
五、watch监视属性
1.watch
-
与Vue2.x中watch配置功能一致
-
两个小“坑”:
- 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效。
//情况一:监视ref定义的响应式数据 watch(sum,(newValue,oldValue)=>{ console.log('sum变化了',newValue,oldValue) },{immediate:true}) //情况二:监视多个ref定义的响应式数据 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变化了',newValue,oldValue) }) /* 情况三:监视reactive定义的响应式数据 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!! 若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 */ watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) },{immediate:true,deep:false}) //此处的deep配置不再奏效 //情况四:监视reactive定义的响应式数据中的某个属性 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //情况五:监视reactive定义的响应式数据中的某些属性 watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //特殊情况 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
2.watchEffect函数
- watch的套路是:既要指明监视的属性,也要指明监视的回调。
- watchEffect的套路是:不用指令监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
- watchEffect有点像computed:
- 但computed注重的是计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
// watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调
watchEffect(() => {
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})
高级侦听器补充
六、组件的生命周期
使用setup
语法糖模式是没有beforeCreate
和created
这两个周期的。
所以生命周期的一般顺序是:
Setup => Mounted => Updated => unMounted
组件A
<template>
<h2>hello!!</h2>
<!-- 测试Mounted前后 -->
<h2>测试Mounted前后</h2>
<div ref="div">{{ divContent }}</div>
<!-- 测试Updated前后 -->
<h2>测试Updated前后</h2>
<button @click="update">update</button>
</template>
<script setup lang="ts">
import {
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onUnmounted,
onUpdated,
ref,
} from "vue";
let num = ref(0);
let divContent = ref<string>("content");
let div = ref<HTMLDivElement>();
const update = () => {
num.value++;
divContent.value = "我被改啦";
};
console.log("setup");
onBeforeMount(() => {
console.log("创建之前===>", div.value);
});
onMounted(() => {
console.log("创建完成===>", div.value);
});
onBeforeUpdate(() => {
console.log("更新之前===>", div.value?.innerText);
});
onUpdated(() => {
console.log("更新完成===>", div.value?.innerText);
});
onBeforeUnmount(() => {
console.log("卸载之前===>");
});
onUnmounted(() => {
console.log("卸载完成===>");
});
</script>
<style lang="scss" scoped></style>
App.vue
<template>
<A v-if="flag"></A>
<hr />
<!-- 测试UnMounted前后 -->
<h2>测试UnMounted前后</h2>
<button @click="change">unmounted</button>
</template>
<script setup lang="ts">
import A from "./components/A.vue";
import { ref } from "vue";
let flag = ref<Boolean>(true);
const change = () => {
flag.value = !flag.value;
};
</script>
<style lang="scss" scoped></style>
总结:
- 使用
setup
语法糖模式是没有beforeCreate
和created
这两个周期的。 onBeforeMount
是读不到dom
的,onMounted
可以读取到dom
的。onBeforeUpdated
获取的是更新之前的dom
,onUpdated
获取的是更新之后的dom
。- 使用
v-if
可以测试unMouned
七、scss基本使用与BEM命名方式
1.scss
SCSS(Sass CSS)是CSS的一种较新的形式。它是基于 CSS3 的一个把 CSS 的语法进行了扩展的版本,因此,所有标准的 CSS 都是合法的 SCSS 代码。在 SCSS 中可以使用变量、嵌套、混入、函数等高级功能,极大地提高了 CSS 的编写效率。下面是 SCSS 的一些常用语法:
- 变量的定义:使用$符号定义变量,例如:
$primary-color: #2196f3;
- 嵌套:可以在样式规则中嵌套子规则,例如:
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li { display: inline-block; }
a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}
- 混入(Mixin):可以定义一段样式,并在需要使用的地方调用,例如:
@mixin square($size: 50px, $color: red) {
width: $size;
height: $size;
background-color: $color;
}
.box {
@include square(100px, blue);
}
- 继承(Extend):可以让一个选择器继承另外一个选择器的所有样式,例如:
.message {
border: 1px solid #ccc;
padding: 10px;
}
.success {
@extend .message;
border-color: green;
}
- 函数:可以定义一些常用的函数,例如:
@function double($n) {
@return $n * 2;
}
以上是 SCSS 的一些常用语法,当然还有很多其他的功能和特性,需要根据具体的需要进行学习和了解。
2.BEM
BEM(Block Element Modifier)是一种前端开发中常用的 CSS 类命名方式,它可以帮助我们编写出易于维护和扩展的代码。
BEM 的基本思想是将页面分解为多个块(Block),每个块包含多个元素(Element),而一个元素可以是另一个块的子块。块和元素分别由名称和描述符组成,这些名称和描述符之间使用双下划线和双短横线来分隔,例如:
/* 定义一个名为 list 的块 */
.list { … }
/* 定义一个名为 item 的元素 */
.list__item { … }
/* 定义一个名为 icon 的修饰符 */
.list__item--icon { … }
其中,list
是块的名称,item
是元素的名称,icon
是修饰符的名称。通过这种方式定义类名,可以使得 HTML 结构和 CSS 样式更加清晰、易于理解和扩展。
具体来说,BEM 将页面中的每个模块都视为独立的“块”,在 CSS 中以 .block
的形式进行命名,例如 .header
、.menu
、.button
等。然后再将一个块内部的各个元素视为该块的组成部分,以 .block__element
的形式进行命名,例如 .header__logo
、.menu__item
、.button__icon
等。需要注意的是,一个元素只能属于一个块,并且必须包含在该块的范围内。
如果需要对某个元素进行样式修饰,则可以使用修饰符(Modifier),以 .block__element--modifier
的形式进行命名,例如.button__icon--small
、.menu-item--active
等。修饰符可以对一个或多个元素进行样式修改,以实现不同状态或风格的变化。
Element UI
就采用了 BEM(Block Element Modifier)的命名方式来定义组件样式类。
在 Element UI
中,每个组件都被视为一个块(Block),并以 el-
作为前缀进行命名,例如 el-button
、el-input
等。每个组件包含多个元素(Element),并以 __
进行分隔,例如 el-button__icon
、el-input__prefix
等。同时,也可以为一个元素添加修饰符(Modifier),以 -
进行分隔,例如 el-button--primary
、el-input--disabled
等。
具体来说,通过 BEM 的命名方式,可以使 Element UI 的组件结构更加清晰,易于理解和修改,同时也方便了其他开发人员对组件样式的继承和覆盖。在使用 Element UI 组件时,我们只需要在 HTML 元素中指定相应的 CSS 类名即可,例如:
<el-button class="el-button--primary">确定</el-button>
上述代码中,我们为 el-button 组件添加了一个 el-button--primary
的修饰符,以实现按钮的主题色为蓝色。
总之,在使用 Element UI
组件时,建议按照 BEM 命名方式规范命名 CSS 类名,这样既能保证代码风格的统一性,也能提高代码的可读性和可维护性。
案例:配置全局BEM全局方式
配置文件bem.scss
$block-sel: "-" !default; // B
$element-sel: "__" !default; // E
$modifier-sel: "--" !default; // M
$namespace: 'xw' !default;
@mixin bfc {
height: 100%;
overflow: hidden;
}
// .xw-block {
// content
// }
// 混入
@mixin b($block) {
$B: $namespace + $block-sel + $block; // 变量
.#{$B} { // 插值语法#{name}
@content; // @content相当于一个占位符,会把自定义样式里的内容替换进来
}
}
@mixin flex {
display: flex;
}
// .xw-block__inner {
// content
// }
@mixin e($element) {
$selector: &; // & 父选择器引用符,此处使用定义变量$selector,来获取父级的类名,如xw-block
@at-root { // 控制指令,用于将样式规则返回到其顶级作用域中,使用嵌套规则时非常有用,可避免样式规则嵌套过深,此处跳出嵌套
#{$selector + $element-sel + $element} {
@content;
}
}
}
@mixin m($modifier) {
$selector: &;
@at-root {
#{$selector + $modifier-sel + $modifier} {
@content
}
}
}
修改vite.config.ts
文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/bem.scss";`
}
}
}
})
测试BEM配置
App.vue
<template>
<div class="xw-test">
block
<div class="xw-test__inner">element</div>
<div class="xw-test--success">modify</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
@include b(test) {
// @include指令被用来引入某个mixin的样式规则
color: green;
@include e(inner) {
color: blue;
}
@include m(success) {
color: red;
}
}
</style>
八、配置局部、全局、递归组件
1.配置局部组件
局部组件也就是常规的在components
文件夹中的定义组件,然后在页面导入并引用组件。
2.配置全局组件
例如组件使用频率非常高,如table、input、button等这些组件,几乎每个页面都在使用,这类组件就可以封装成全局组件。
我们也可以在components
文件夹中自定义频繁使用的组件,并将自定义组件注册成全局组件。
将注册成全局组件的方法都是一样的,看如下案例:
我们在这封装一个Card组件,想在任何地方去使用
<template>
<div class="card">
<header>
<div>标题</div>
<div>副标题</div>
</header>
<section>{{ content }}</section>
</div>
</template>
<script setup lang="ts">
// import { ref, reactive } from "vue";
defineProps({
content: {
type: String,
default: "",
},
});
</script>
<style lang="less" scoped>
@border: #ccc;
.card {
border: 1px solid @border;
width: 400px;
header {
display: flex;
justify-content: space-between;
padding: 5px;
border-bottom: 1px solid @border;
}
section {
padding: 5px;
min-height: 300px;
}
}
</style>
在main.ts
中注册全局组件
import { createApp } from 'vue'
import App from './App.vue'
// 导入组件
import CardVue from './components/Card.vue'
export const app = createApp(App)
// 注册全局组件
app.component('Card', CardVue)
app.mount('#app')
在其他文件中使用全局组件
<template>
<div>
<!-- <TreeVue :data="data"></TreeVue> -->
<Card content="this is content"></Card>
</div>
</template>
结果展示:
3.配置递归组件
递归组件的原理和我们写js递归一样,自己调用自己,然后通过一个条件来结束递归,否则导致内存泄漏。
这里我们写一个简易的递归树案例
Tree.vue
<template>
<div
@click.stop="chilkTap(item, $event)"
v-for="item in data"
:key="item.name"
class="tree"
>
<input type="checkbox" v-model="item.checked" />
<span>{{ item.name }}</span>
// 自己调用自己,并且使用v-if和?扩展运算符来终止递归
<Tree v-if="item?.children?.length" :data="item?.children"></Tree>
</div>
</template>
<script setup lang="ts">
interface Tree {
name: string;
checked: boolean;
children?: Tree[];
}
defineProps<{
data?: Tree[];
}>();
// 节点点击事件
const chilkTap = (item: Tree, e: any) => {
console.log("item:", item);
console.log("event", e);
};
</script>
<style lang="scss" scoped>
.tree {
margin-left: 10px;
}
</style>
在其他页面使用递归组件
<template>
<div>
<TreeVue :data="data"></TreeVue>
</div>
</template>
<script setup lang="ts">
import {reactive } from "vue";
import TreeVue from "./components/Tree.vue";
interface Tree {
name: string;
checked: boolean;
children?: Tree[];
}
// mock一个三层结构的静态数据
const data = reactive<Tree[]>([
{
name: "1",
checked: false,
children: [{ name: "1-1", checked: false }],
},
{
name: "2",
checked: false,
},
{
name: "3",
checked: false,
children: [
{
name: "3-1",
checked: false,
children: [{ name: "3-1-1", checked: false }],
},
],
},
]);
</script>
结果展示:
九、动态组件
什么是动态组件:
让多个组件使用同一个挂载点,并动态切换,这就是动态组件。
用法如下:
引入组件
import A from './A.vue'
import B from './B.vue'
通过is切换A、B组件
<component :is="A"></component>
使用场景:tab切换
下面来看案例:
定义三个子组件,分别是B、C、D。
<template>
<div style="display: flex">
<div
@click="switchCom(item, index)"
:class="[active == index ? 'active' : '']"
class="tabs"
v-for="(item, index) in data"
:key="item.name"
>
<div>{{ item.name }}</div>
</div>
</div>
// 此处调用动态组件
<component :is="comId"></component>
</template>
<script setup lang="ts">
import { ref, reactive, shallowRef, markRaw } from "vue";
// 引入子组件
import B from "./components/B.vue";
import C from "./components/C.vue";
import D from "./components/D.vue";
// 默认激活的子组件
const comId = shallowRef(B);
// 默认激活的tab索引
const active = ref(0);
// 通过data来管理子组件,以便动态调用子组件
const data = reactive([
{
name: "B组件",
// markRaw作用:使其在被响应式代理时不会被视为响应式数据
com: markRaw(B),
},
{
name: "C组件",
com: markRaw(C),
},
{
name: "C组件",
com: markRaw(D),
},
]);
// tab切换触发的组件切换
const switchCom = (item, index) => {
comId.value = item.com;
active.value = index;
};
</script>
<style lang="scss" scoped>
.active {
background-color: skyblue;
}
.tabs {
border: 1px solid #ccc;
margin: 10px;
padding: 10px;
cursor: pointer;
}
</style>
效果展示:
注意事项:
1.在Vue2 的时候is 是通过组件名称切换的 在Vue3 setup 是通过组件实例切换的
2.如果你把组件实例放到Reactive Vue会给你一个警告runtime-core.esm-bundler.js:38 [Vue warn]: Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw
or using shallowRef
instead of ref
.
Component that was made reactive:
这是因为reactive 会进行proxy 代理 而我们组件代理之后毫无用处,为了节省性能开销,推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理。
十、异步组件
异步组件是什么?
异步组件是一种在 Vue.js 应用程序中,延迟加载组件的机制。当应用程序需要加载某个组件时,使用异步组件可以将该组件从应用程序的主要代码中分离出来,以实现按需加载和提高应用程序的初始性能。
传统的情况下,所有组件都是同步加载的,这意味着当用户打开应用程序时,所有组件都会立即被加载并注册到 Vue.js 实例中。随着应用程序变得更加庞大,这些组件的数量和大小也会增加,导致应用程序加载速度变慢,甚至影响用户体验。使用异步组件机制,可以将某些不常用的组件或者较为耗时的组件单独分离出来,延迟加载,从而提高应用程序性能。
在 Vue 2 中,可以通过 require.ensure
或者 Webpack 的 import()
函数来实现异步组件。而在 Vue 3 中,则提供了新的 API:defineAsyncComponent
来实现异步组件的定义。
异步组件的应用场景
-
当应用程序比较庞大时,需要优化应用程序的加载速度和时间。在这种情况下,将组件拆分成多个小模块,并使用异步加载以降低应用程序的初始负载就非常重要了。
如骨架屏的实现
-
除了优化应用程序的启动性能之外,还可以通过按需加载组件来减小应用程序的总体大小。例如,在一个包含多个不同页面的单页应用程序中,可能只有某些页面才需要特定的组件或功能。如果将所有组件都打包到应用程序中,则会增加整体应用程序的。
顶层await
在 Vue 3 中,await 可以被用于异步组件的定义中。在组件定义对象中,可以使用 load 函数来定义加载组件的异步操作,例如:
const AsyncComponent = {
name: 'AsyncComponent',
async load() {
const component = await import('./MyComponent.vue');
return component.default;
},
render() {
return h('div', [this.Component ? h(this.Component) : null])
}
};
在上面的代码中,load 函数是一个 async 函数,它通过 import 语句异步地加载了我们想要的组件,并返回该组件的默认导出值。
在 render 函数中,我们使用 this.Component 来渲染异步加载的组件。当异步组件还没有加载完成时,this.Component 的值为 null,所以这里使用了条件判断来避免出现“渲染组件未定义”的错误。
总之,Vue 3 中的 await 可以被用于异步组件的定义中,帮助我们更方便地实现组件的异步加载。
<script setup>
中可以使用顶层 await。结果代码会被编译成 async setup()
。
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
父组件引用子组件 通过defineAsyncComponent加载异步配合import 函数模式便可以分包
<script setup lang="ts">
import { reactive, ref, markRaw, toRaw, defineAsyncComponent } from 'vue'
const Dialog = defineAsyncComponent(() => import('../../components/Dialog/index.vue'))
//完整写法
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
suspense
<suspense>
组件有两个插槽。它们都只接收一个直接子节点。default
插槽里的节点会尽可能展示出来。如果不能,则展示 fallback
插槽里的节点。
<Suspense>
<!-- default -->
<template #default>
<Dialog>
<template #default>
<div>我在哪儿</div>
</template>
</Dialog>
</template>
<!-- fallback -->
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
十一、teleport
teleport是Vue3.0新特性之一。
teleport是一种能够将我们的模板渲染之指定DOM节点,不受父级syle、v-show等属性影响,但data、prop数据依旧能够共用的技术,类似于React的Protal。
teleport的主要应用场景包括:
- 模态框和弹出框:在用户点击按钮后,模态框或弹出框通常需要动态地插入到页面的某个节点上。使用 Teleport 可以更方便地实现这个操作。例如:
<button @click="showModal = true">打开模态框</button>
<teleport to="body">
<modal v-if="showModal" @close="showModal = false"></modal>
</teleport>
- 提示和通知:当需要在页面的某个位置显示提示或通知时,Teleport 也会变得非常有用。例如,可以在 HTML 文档的末尾添加一个容器元素,用于显示全局的消息提示:
<template>
<div>
<button @click="showMessage()">显示消息</button>
<teleport to="#message-container">
<div class="message" v-if="message">{{ message }} </div>
</teleport>
</div>
<div id="message-container"></div>
</template>
<script>
export default {
data() {
return {
message: ''
}
},
methods: {
showMessage() {
this.message = '这是一条消息';
setTimeout(() => {
this.message = '';
}, 3000);
}
}
}
</script>
- 图片懒加载:Teleport 还可以用于实现图片懒加载。通常,我们需要在滚动到指定位置时再将某些元素插入页面中,以避免资源的浪费。此时,Teleport 提供了一个很好的解决方案,例如:
<template>
<div>
<teleport to="#image-container">
<img :src="imageSrc" @load="handleImageLoad" />
</teleport>
</div>
<div id="image-container"></div>
</template>
<script>
export default {
data() {
return {
imageSrc: '',
loaded: false
}
},
mounted() {
window.addEventListener('scroll', this.loadImage);
},
beforeUnmount() {
window.removeEventListener('scroll', this.loadImage);
},
methods: {
handleImageLoad() {
this.loaded = true;
},
loadImage() {
if (this.loaded) return;
const container = document.querySelector('#image-container');
const rect = container.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
this.imageSrc = 'image.jpg';
}
}
}
}
</script>
总之,Teleport 能够很好地解决组件渲染的问题,在模态框、提示和通知、图片懒加载等场景中表现非常出色。
下面通过一个项目中常见的案例来体会一下teleport
的妙用:
我们定义一个使用position:absolute
的方式实现提示框在可视区垂直、水平居中显示的子组件。
子组件Dialog
代码:
<template>
<div class="dialog">
<header><div>提示:</div></header>
<main>
<hr />
<div>content</div>
</main>
</div>
</template>
<style lang="scss" scoped>
.dialog {
width: 300px;
height: 300px;
background-color: #ccc;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
当父组件没有用position: relative;
时,对话框成功实现垂直、水平居中。
但当父组件使用position: relative;
后,对话框将无法实现在可视区垂直、水平居中。代码如下:
<template>
<div class="father">
我是父组件
<Dialog></Dialog>
</div>
</template>
<script setup lang="ts">
// import { ref, reactive } from 'vue'
import Dialog from "./components/Dialog.vue";
</script>
<style lang="scss" scoped>
.father {
height: 500px;
background-color: yellow;
position: relative;
}
</style>
结果如下:
这时,我们就可以使用teleport
来实现将子组件Dialog
渲染到指定DOM节点body
。
代码如下:
<template>
<div class="father">
我是父组件
<teleport to="body">
<Dialog></Dialog>
</teleport>
</div>
</template>
<script setup lang="ts">
// import { ref, reactive } from 'vue'
import Dialog from "./components/Dialog.vue";
</script>
<style lang="scss" scoped>
.father {
height: 500px;
background-color: yellow;
position: relative;
}
</style>
结果如下:
十二、keep-alive缓存组件
Vue中keep-alive的深入理解和使用
十三、transition动画组件
在 Vue 中,Transition 是一种动画效果的实现方式。在组件切换时,可以通过添加 Transition 的相关属性来实现过渡动画效果。
使用 Transition 可以让我们的应用程序更加生动、具有交互性。例如,在两个组件之间进行切换时,添加 Transition 效果可以使界面转换更加自然、平滑,提升用户的体验感受。
1.过渡的类名
在进入/离开的过渡中,会有6个class切换。
- v-enter-from:定义进入过渡的开始状态,在元素被插入之前生效,在元素被插入之后的下一帧移除。
- v-enter-active:定义进入过渡生效时的状态,在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
- v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效(与此同时v-enter-from被移除),在过渡/动画完成之后移除。
- v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
- v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
- v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
案例:
<template>
<div>
<button @click="flag = !flag">切换</button>
<transition name="fade">
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import "animate.css";
let flag = ref(true);
</script>
<style lang="scss" scoped>
.box {
width: 300px;
height: 300px;
background-color: red;
}
// 开始过渡DOM的初始状态
.fade-enter-from {
width: 0;
height: 0;
transform: rotate(360deg);
background-color: red;
}
// 开始过渡到过渡完成的过程
.fade-enter-active {
transition: all 1.5s linear;
}
// 定义过渡结束的Dom状态,即过渡当这个状态时结束
.fade-enter-to {
background-color: yellow;
width: 100px;
height: 100px;
}
// 离开的过渡
.fade-leave-from {
width: 200px;
height: 200px;
transform: rotate(360deg);
}
// 离开的过渡过程
.fade-leave-active {
transition: all 1s linear;
}
//离开完成
.fade-leave-to {
width: 100px;
height: 50px;
}
</style>
2.自定义过渡class类名
transition动画组件有如下过渡class类名(props)
- enter-from-class
- enter-active-class
- enter-to-class
- leave-from-class
- leave-active-class
- leave-to-class
还可以使用duration
属性定义过渡时间。
<transition enter-from-class="" enter-active-class="" enter-to-class="" leave-from-class="" leave-active-class="" leave-to-class="" >...</transition>
<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
3.animate动画库的使用
官方文档 Animate.css | A cross-browser library of CSS animations.
安装animate库:$ npm install animate.css --save
基本用法:<h1 class="animate__animated animate__bounce">An animated element</h1>
copy此类名,调用时加上类名animate__animated
动画效果就能生效。
结合transition动画组件自定义库使用的案例:
<template>
<div>
<button @click="flag = !flag">切换</button>
<transition
leave-active-class="animate__animated animate__bounce"
enter-active-class="animate__animated animate__flash"
>
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
十四、transition-group过渡列表
1. 过渡列表的简单用法
<transition-group>
是 Vue.js 内置的一个高级动画组件,它可以帮助我们实现在添加或删除元素时,自动应用过渡动画。通常与 <v-for>
一起使用,当数据改变时,<transition-group>
会自动检测哪些元素被添加或删除,然后应用相应的过渡动画。
<transition-group>
在使用上与 <transition>
组件类似,但它能够对一组元素进行过渡效果的处理。常见的应用场景包括列表渲染,在增加或删除列表项时,以平滑动画效果来显示添加和删除的元素,从而增强用户体验。
下面看一个简单的案例:
<template>
<div><button @click="add">add</button> <button @click="pop">pop</button></div>
<div class="wraps">
<transition-group
leave-active-class="animate__animated animate__bounceOut"
enter-active-class="animate__animated animate__fadeInDownBig"
>
<div style="margin: 10px" v-for="item in list" :key="item" class="item">
{{ item }}
</div>
</transition-group>
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import "animate.css";
const list = reactive<number[]>([1, 2, 3, 4, 5]);
const add = () => {
list.push(list.length + 1);
};
const pop = () => {
list.pop();
};
</script>
<style lang="scss" scoped>
.wraps {
display: flex;
flex-wrap: wrap;
}
.item {
padding: 10px;
font-size: 20px;
border: 1px solid #ccc;
}
</style>
结果展示:
2.过渡列表的移动过渡
<transition-group>
组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。
只需要为<transition-group>
组件的move-class
属性绑定自定义的过渡类别就行,我们通过以下案例来体会移动过渡。
<template>
<div>
<button @click="random">random</button>
<transition-group move-class="mmm" tag="div" class="wraps">
<div :key="item.id" v-for="item in list" class="item">
{{ item.number }}
</div>
</transition-group>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import _ from "lodash";
// 生成9*9的列表数据
let list = ref(
Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1,
};
})
);
// console.log(list.value);
const random = () => {
// 使用lodash库的_.shuffle()方法实现列表内的数据顺序打乱
list.value = _.shuffle(list.value);
};
</script>
<style lang="scss" scoped>
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(27px * 9);
.item {
height: 25px;
width: 25px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
}
}
// 过渡动画
.mmm {
transition: all 1s;
}
</style>
结果展示:
3.状态过渡
十五、mitt事件总线库
mitt
是一个小巧的 JavaScript 事件总线库,可以用于前端项目中轻松地实现自定义事件的管理和监听。
它是按照发布-订阅模式设计的,可以在任何地方使用,例如 React、Vue、Angular 等前端框架中。相比于 Vue.js 或 React 内置的事件系统,mitt
更为灵活,不需要依赖特定的框架或库。
以下是一个简单的示例:
import mitt from 'mitt'
// 创建一个事件总线
const eventBus = mitt()
// 监听事件
eventBus.on('event1', data => {
console.log('event1', data)
})
// 触发事件
eventBus.emit('event1', 'hello world')
// 移除事件监听器
eventBus.off('event1', listener)
下面我们使用mitt
实现全局配置事件总线:
配置入口文件main.ts
import { createApp } from 'vue'
import App from './App.vue'
// import CardVue from './components/Card.vue'
import mitt from 'mitt'
const Mit = mitt()
export const app = createApp(App)
// 注册全局组件
// app.component('Card', CardVue)
// 这段代码利用了ts module declaration的特性,声明了一个名为vue的模块
// 并通过ComponentCustomProperties接口扩展了Vue.js组件实例类型。
// 其中ComponentCustomProperties是一个内置的Vue.js接口,可用于向组件实例添加自定义属性。
declare module 'vue' {
export interface ComponentCustomProperties {
// 通过$Bus: typeof Mit 的语法,我们将$Bus属性的类型指定为Mit类型的的类型本身,
// 也就是自定义事件总线库的类型,这样对于使用TS的项目,就可以在组件中
// 直接访问到$Bus属性,并且享受到类型检查的好处。
$Bus: typeof Mit
}
}
// 使用mitt注册全局事件Bus
app.config.globalProperties.$Bus = Mit
app.mount('#app')
父组件App.vue
<template>
<!-- 兄弟组件B、C -->
<B></B>
<C></C>
</template>
<script setup lang="ts">
import B from "./components/B.vue";
import C from "./components/C.vue";
</script>
通过兄弟组件中的B组件向C组件派发事件,C组件触发事件
B.vue
<template>
<h1>我是B</h1>
<button @click="emit">按钮</button>
</template>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const emit = () => {
instance?.proxy?.$Bus.emit("on-xiao5", "mitt");
instance?.proxy?.$Bus.emit("on-xiao4", "mitt2");
instance?.proxy?.$Bus.emit("on-xiao3", "mitt3");
};
</script>
C.vue
<template>
<h1>C</h1>
</template>
<script setup lang="ts">
// import { ref, reactive } from 'vue'
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
// 监听一个事件
instance?.proxy?.$Bus.on("on-xiao5", (str) => {
console.log(str);
});
// 监听多个事件"*",回调函数的第一个参数是事件类型(事件名),第二个参数是传的参
instance?.proxy?.$Bus.on("*", (type, str) => {
console.log("type:", type);
console.log("value:", str);
});
// 删除一个事件
// instance?.proxy?.$Bus.off("on-xiao5", () => {
// console.log("off over");
// });
// 删除全部事件
// instance?.proxy?.$Bus.all.clear();
</script>
结果展示:
十六、TSX
参考博客:学习Vue3 第二十五章(TSX)
TSX 是一种在 TypeScript 中编写 React 组件的语法,相比于传统的 JSX 语法,它能提供更好的类型检查和编译时错误检查。TSX 其实就是将 JSX 和 TypeScript 结合起来,所以可以在 TSX 中使用 JSX 的所有语法,同时也可以使用 TypeScript 的类型注解来为组件和组件 Props 声明类型。通常,TSX 的文件扩展名为 “.tsx”。
安装tsx插件:npm install @vitejs/plugin-vue-jsx -D
修改vite.config.ts
配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 导入tsx插件
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
// 启动tsx插件
plugins: [vue(),vueJsx()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/bem.scss";`
}
}
}
})
在 TSX 中,需要遵循以下规范:
- 文件名应该以
.tsx
为后缀。 - 组件名应该以大写字母开头并使用驼峰式大小写,如 MyComponent。
- JSX 不支持
v-bind
,使用{}
来代替,例如value = { this.props.name }
。 - JSX 中不支持
if
语句,可以使用三目运算符代替,例如{ condition ? true : false }
。 - JSX 中不支持
for
循环,可以使用Array.map()
函数来代替,例如{ this.props.items.map(item => <li>{item}</li>) }
。 - 支持
v-show
。 ref
在template
自动解包.value
,但tsx不会,要手动加.value
。
案例代码:
App.tsx
// 写法一
// export default function () {
// return (<div>小5</div>)
// }
// 写法二:optionsAPI
// import { defineComponent } from "vue"
// export default defineComponent({
// data() {
// return {
// age: 23
// }
// },
// render() {
// return (<div>{this.age}</div>)
// }
// })
// 写法三:setup
import { defineComponent, ref } from "vue";
// 组件A
const A = (_:any, { slots }) => (
<>
<div>{slots.default ? slots.default() : '默认值'}</div>
</>
)
interface Props {
name?: string
}
export default defineComponent({
props: {
name: String
},
emits: ['on-click'],
setup(props: Props, {emit}) {
const flag = ref(false)
const data = [
{
name: '小5——1'
},
{
name: '小5——2'
},
{
name: '小5——3'
}
]
const fn = (value: any) => {
console.log('触发了', value);
// 派发事件
emit('on-click', value)
}
const slot = {
default: () => (<div>小5default slots</div>)
}
const modelValue = ref()
return () => (
<>
<h5>测试父组件向子组件传递数据</h5>
<div>props:{props?.name}</div>
<hr />
<h5>测试map代替v-for</h5>
{
data.map(item => {
return (<div>{item.name}</div>)
})
}
<hr />
<h5>测试三元表达式代替v-if</h5>
<div>{flag.value ? <div>true</div> : <div>false</div>}</div>
<hr />
<h5>测试事件派发和监控</h5>
<button onClick={() => fn('尼赣嘛')}>按钮</button>
<hr />
<h5>测试插槽</h5>
<A v-slots={slot}></A>
<hr />
<h5>测试v-model</h5>
<input type="text" v-model={modelValue.value} />
<div>{modelValue.value}</div>
</>
)
}
})
App.vue
<template>
<div>
<xiao5 name="小6" @on-click="handleOn"></xiao5>
</div>
</template>
<script setup lang="ts">
// 导入tsx
import xiao5 from "./App";
const handleOn = (value: any) => {
console.log("父组件中触发子组件派发的事件");
console.log("父组件收到子组件派发事件传来的参数:", value);
};
</script>
结果展示:
十七、深入v-model
在 Vue 3 中,使用 v-model
与 Vue 2 中的用法略有不同,主要是因为 Vue 3 中有了新的 setup()
函数和 reactive()
函数,这些函数使得我们可以更方便地编写组件逻辑。
在 Vue 3 中,v-model
可以使用 modelValue
和 update:modelValue
两个属性来实现双向数据绑定。
1.v-model绑定一个值
案例:使用双向绑定v-model
实现点击切换开关控制子组件的显示与隐藏,并可以通过关闭按钮隐藏子组件。
父组件App.vue
<template>
<div>
<h3>我是父组件</h3>
<div>isShow:{{ isShow }}</div>
<button @click="isShow = !isShow">切换</button>
<A v-model="isShow"></A>
</div>
</template>
<script setup lang="ts">
import A from "./components/A.vue";
import { ref } from "vue";
const isShow = ref<boolean>(true);
</script>
子组件A.vue
:
<template>
<div class="main" v-if="modelValue">
<h3>我是子组件</h3>
<div>content</div>
<button class="btn" @click="close">关闭</button>
</div>
</template>
<script setup lang="ts">
// 接收父组件v-model传来的值,并用modelValue代替
defineProps<{
modelValue: boolean;
}>();
// 定义更新modelValue的emit,实现双向绑定
const emit = defineEmits(["update:modelValue"]);
// 通过事件触发emit
const close = () => {
emit("update:modelValue", false);
};
</script>
<style lang="scss" scoped>
.main {
position: relative;
left: 200px;
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
.btn {
position: absolute;
top: 0;
right: 0;
}
</style>
结果展示:
2.v-model绑定多个值
案例:继上一个案例,实现多个值的双向绑定。
父组件App.vue
<template>
<div>
<h3>我是父组件</h3>
<div>isShow:{{ isShow }}</div>
<div>text:{{ text }}</div>
<button @click="isShow = !isShow">切换</button>
<!-- 传多个v-model -->
<A v-model="isShow" v-model:textVal="text"></A>
</div>
</template>
<script setup lang="ts">
import A from "./components/A.vue";
import { ref } from "vue";
const isShow = ref<boolean>(true);
const text = ref<string>("小5");
</script>
子组件A.vue
:
<template>
<div class="main" v-if="modelValue">
<h3>我是子组件</h3>
<div>content</div>
<button class="btn" @click="close">关闭</button>
<div>内容:<input type="text" :value="textVal" @input="change" /></div>
</div>
</template>
<script setup lang="ts">
// 同时接收父组件v-model传来的多个值
defineProps<{
modelValue: boolean;
textVal: string;
}>();
// 定义更新多个值的emit
const emit = defineEmits(["update:modelValue", "update:textVal"]);
// 通过事件触发emit
const close = () => {
emit("update:modelValue", false);
};
const change = (e: Event) => {
const target = e.target as HTMLInputElement;
emit("update:textVal", target.value);
};
</script>
结果展示:
3.自定义修饰符
在Vue 3中,你可以使用自定义修饰符来改变v-model指令的行为。自定义修饰符应该用.符号来表示,例如.number和.trim。
// 同时接收父组件v-model传来的多个值
defineProps<{
modelValue: boolean;
textVal: string;
textValModifiers?: { // 值+Modidiers
default: () => {
}
}>();