2022 Vue3+vite+ts (从零入门学习干货+案例代码,更新中)
目录
一、vue介绍
二、教你创建新项目
三、介绍Vue3 模板语法、插入指令
四、介绍虚拟Dom和Diff算法
五、认识ref全家桶
六、认识reactive全家桶
七、认识to系列的全家桶
八、认识computed计算属性
九、认识watch监听器
十、认识watchEffect高级侦听器
十一、认识组件&Vue3生命周期
十二、实操组件和认识 Less & Scoped
十三、父子组件传参
十四、使用全局组件、局部组件、递归组件
十五、简单实现动态组件
十六、认识插槽全家桶
十八、Teleport 传送组件
十九、vue-router 之 keep-alive 缓存组件
二十、transition动画组件
二十一、transition-group过渡列表
二十二、 更新中...
一、vue介绍
官宣:
Vue (读音 /vjuː/,类似于 view ) 是一套用于构建用户界面的 渐进式框架 。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与 现代化的工具链 以及各种 支持类库 结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
1、最主要的:重写双向绑定
vue2
基于Object.defineProperty()实现
vue3 基于Proxy
proxy与Object.defineProperty(obj, prop, desc)方式相比有以下优势:
//丢掉麻烦的备份数据
//省去for in 循环
//可以监听数组变化
//代码更简化
//可以监听动态新增的属性;
//可以监听删除的属性 ;
//可以监听数组的索引和 length 属性;
let proxyObj = new Proxy(obj,{
get : function (target,prop) {
return prop in target ? target[prop] : 0
set : function (target,prop,value) {
target[prop] = 888;
2、Vue3 优化Vdom
在Vue2中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对比消耗
patch flag 优化静态树
新增了 patch flag 标记
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
BALL = -2
我们发现创建动态 dom 元素的时候,Vdom 除了模拟出来了它的基本信息之外,还给它加了一个标记: 1 /* TEXT */
这个标记就叫做 patch flag(补丁标记)
patch flag 的强大之处在于,当你的 diff 算法走到 _createBlock 函数的时候,会忽略所有的静态节点,只对有标记的动态节点进行对比,而且在多层的嵌套下依然有效。
尽管 JavaScript 做 Vdom 的对比已经非常的快,但是 patch flag 的出现还是让 Vue3 的 Vdom 的性能得到了很大的提升,尤其是在针对大组件的时候。
3、Vue3 Fragment
vue3 允许我们支持多个根节点
<template> <div>12</div> <div>23</div> </template>
同时支持render JSX 写法
render() {
return (
{this.visable ? (
<div>{this.obj.name}</div>
) : (
<div>{this.obj.price}</div>
<input v-model={this.val}></input>
{[1, 2, 3].map((v) => {
return <div>{v}-----</div>;
同时新增了Suspense 和 多 v-model 用法
4、Vue3 Tree shaking
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。
而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中
比如你要用watch 就是import {watch} from 'vue' 其他的computed 没用到就不会给你打包,减少体积
5、Vue 3 Composition Api
Setup 函数式编程 也叫vue Hook
例如 ref reactive watch computed toRefs toRaws
二、创建新项目
1、安装node(装过的忽略,-V确定一下版本)
https://www. runoob.com/nodejs/nodej s-install-setup.html
2、构建vite项目
官方文档开始 开始 Vite中文网
vite 的优势
冷服务 默认的构建目标浏览器是能 在 script 标签上支持原生 ESM 和 原生 ESM 动态导入
HMR 速度快到惊人的 模块热更新(HMR)
Rollup打包 它使用 Rollup 打包你的代码,并且它是预配置的 并且支持大部分rollup插件
使用vite初始化一个项目
npm
npm init vite@latest
Yarn
yarn create vite
接着设置好项目名称,安装好依赖,就可以开始学习vite了
package json 命令解析
{
"scripts": {
"dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
"build": "vite build", // 为生产环境构建产物
"preview": "vite preview" // 本地预览生产构建产物
3、Vite目录
public 下面的不会被编译 可以存放静态资源
assets 下面可以存放可编译的静态资源
components 下面用来存放我们的组件
App.vue 是全局组件
main ts 全局的ts文件
index.html 非常重要的入口文件 (webpack,rollup 他们的入口文件都是enrty input 是一个js文件 而Vite 的入口文件是一个html文件,他刚开始不会编译这些js文件 只有当你用到的时候 如script src="xxxxx.js" 会发起一个请求被vite拦截这时候才会解析js文件)
vite config ts 这是vite的配置文件具体配置项 后面会详解
VsCode Vue3 插件推荐 Vue Language Features (Volar)
SFC 语法规范 *.vue 件都由三种类型的顶层语法块所组成:<template>、<script>、<style>
<template> 每个 *.vue 文件最多可同时包含一个顶层 <template> 块。
其中的内容会被提取出来并传递给 @vue/compiler-dom,预编译为 JavaScript 的渲染函数,并附属到导出的组件上作为其 render 选项。
<script> 每一个 *.vue 文件最多可同时包含一个 <script> 块 (不包括<script setup>)。
该脚本将作为 ES Module 来执行。
其默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值。
<script setup> 每个 *.vue 文件最多可同时包含一个 <script setup> 块 (不包括常规的 <script>)
该脚本会被预处理并作为组件的 setup() 函数使用,也就是说它会在每个组件实例中执行。<script setup> 的顶层绑定会自动暴露给模板。更多详情请查看 <script setup> 文档。
<style> 一个 *.vue 文件可以包含多个 <style> 标签。
<style> 标签可以通过 scoped 或 module attribute (更多详情请查看 SFC 样式特性) 将样式封装在当前组件内。多个不同封装模式的 <style> 标签可以在同一个组件中混
三、模板语法、指令
1、模板插值语法 或者使用v-text
在script 声明一个变量可以直接在template 使用用法为{{变量名称}}
模板语法是可以编写条件运算的、操作API 也是支持的
<template>
<div>{{ message.split(",").map((v) => `666${v}`) }}</div>
</template>
<script setup lang="ts">
const message: string = "你,是,好,人";
</script>
<style></style>
2、指令
v- 开头都是vue 的指令
v-text 用来显示文本
v-html 用来展示富文本
v-if 用来控制元素的显示隐藏(切换真假DOM)
v-else-if 表示 v-if 的“else if 块”。可以链式调用
v-else v-if条件收尾语句
v-show 用来控制元素的显示隐藏(display none block Css切换)
v-on 简写@ 用来给元素添加事件
v-bind 简写: 用来绑定元素的属性Attr
v-model 双向绑定
v-for 用来遍历元素
v-on修饰符 冒泡案例 .stop阻止事件冒泡
<template>
<div @click="parent">
<div @click.stop="child">child</div>
</template>
<script setup lang="ts">
const child = () => {
console.log('child');
const parent = () => {
console.log('parent');
</script>
阻止表单提交案例
<template>
<form action="/">
<button @click.prevent="submit" type="submit">submit</button>
</form>
</template>
<script setup lang="ts">
const submit = () => {
console.log('child');
</script>
<style>
</style>
v-bind 绑定style案例
<template>
<div :style="style">学习前端</div>
</template>
<script setup lang="ts">
type Style = {
color: string,
height: string
const style:Style = {
color: "blue",
height: "300px"
</script>
v-bind 绑定class案例
<template>
<div :class="flag">{{flag}}</div>
</template>
<script setup lang="ts">
type Cls = {
other: boolean,
h: boolean
const flag: Cls = {
other: true,
h: true
</script>
<style>
.active {
color: red;
.other {
color: blue;
border: 3px solid #ccc;
</style>
v-model 双向绑定案例
<template>
<input v-model="message" type="text" />
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref("v-model123")
</script>
<style>
.active {
color: red;
.other {
color: blue;
height: 300px;
border: 1px solid #ccc;
</style>
四、虚拟Dom和Diff算法
为什么要学习源码
1.可以提升自己学习更优秀的API设计和代码逻辑
2.面试的时候也会经常问源码相关的东西
3.更快的掌握vue和遇到问题可以定位
介绍虚拟Dom
虚拟DOM就是通过JS来生成一个AST节点树
为什么要有虚拟DOM?
- MVVM框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
- 虚拟DOM跟踪状态变化
- 参考github上 virtual-dom [1]的动机描述
- 虚拟DOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实DOM
- 跨平台使用
- 浏览器平台渲染DOM
- 服务端渲染SSR(Nuxt.js/Next.js),前端是vue向,后者是react向
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
- 真实DOM的属性很多,创建DOM节点开销很大
- 虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小
- 复杂视图情况下提升渲染性能 (操作dom性能消耗大,减少操作dom的范围可以提升性能)
我们可以通过下面的例子
let div = document.createElement('div')
let str = ''
for (const key in div) {
str += key + ''
console.log(str)
发现一个dom上面的属性是非常多的
aligntitlelangtranslatedirhiddenaccessKeydraggablespellcheckautocapitalizecontentEditableisContentEditableinputModeoffsetParentoffsetTopoffsetLeftoffsetWidthoffsetHeightstyleinnerTextouterTextonbeforexrselectonabortonbluroncanceloncanplayonca.......文章原因此处省略
所以直接操作DOM非常浪费性能
解决方案就是 我们可以用
JS
的计算性能来换取操作
DOM
所消耗的性能,既然我们逃不掉操作
DOM
这道坎,但是我们可以尽可能少的操作
DOM
灵魂发问
:使用了虚拟DOM就一定会比直接渲染真实DOM快吗? 答案当然是
否定
的,且听我说:
举例 :当一个节点变更时DOMA->DOMB
上述情况:
示例1
是创建一个
DOMB
然后替换掉
DOMA
;
示例2
去
创建虚拟DOM+DIFF算法
比对发现
DOMB
跟
DOMA
不是相同的节点,最后还是创建一个
DOMB
然后替换掉
DOMA
; 可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+DIFF算啊对比 所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是
错误的,不严谨的
举例 :当DOM树里面的某个子节点的内容变更时:
当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像
示例1
重新去渲染这个
DOM树
,这个时候
虚拟DOM+DIFF算法
就能够得到很好的体现,我们通过
示例2
使用
虚拟DOM+Diff算法
去找出改变了的子节点更新它的内容就可以了
总结:
复杂视图情况下提升渲染性能
,因为
虚拟DOM+Diff算法
可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)
介绍Diff算法
在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,Diff算法其实就是找出两者之间的差异;
diff 算法首先要明确一个概念就是 Diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 Diff 算法的结果。
到这里就与snabbdom源码核心部分离不开了
snabbdom的核心
-
init()
设置模块.创建patch()
函数 -
使用
h()
函数创建JavaScript对象(Vnode)
描述真实DOM
-
patch()
比较新旧两个Vnode
-
把变化的内容更新到
真实DOM树
init函数
当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建
虚拟DOM(Vnode)对象
;在上文中就使用了
样式模块
以及
事件模块
让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过
patch函数
对比
两个虚拟dom
(会先把app转换成虚拟dom),更新视图;
h函数
有些地方也会用
createElement
来命名,它们是一样的东西,都是创建
虚拟DOM
的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:
总结
:
h函数
先生成一个
vnode
函数,然后
vnode
函数再生成一个
Vnode
对象(虚拟DOM对象)
补充:
在h函数源码部分涉及一个
函数重载
的概念,简单说明一下:
- 参数个数或参数类型不同的函数()
- JavaScript中没有重载的概念
- TypeScript中有重载,不过重载的实现还是通过代码调整参数
重载这个概念个参数相关,和返回值无关
patch函数(核心)
要是看完前面的铺垫,看到这里你可能走神了,
醒醒啊,这是核心啊,上高地了兄弟
;
- pactch(oldVnode,newVnode)
- 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)
-
对比新旧
VNode
是否相同节点(节点的key和sel相同) - 如果不是相同节点,删除之前的内容,重新渲染
-
如果是相同节点,再判断新的
VNode
是否有text
,如果有并且和oldVnode
的text
不同直接更新文本内容(patchVnode)
-
如果新的VNode有children,判断子节点是否有变化
(updateChildren,最麻烦,最难实现)
源码:
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
// cbs.pre就是所有模块的pre钩子函数集合
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// isVnode函数时判断oldVnode是否是一个虚拟DOM对象
if (!isVnode(oldVnode)) {
// 若不是即把Element转换成一个虚拟DOM对象
oldVnode = emptyNodeAt(oldVnode)
// sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;
if (sameVnode(oldVnode, vnode)) {
// 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值
// parentNode就是获取父元素
parent = api.parentNode(elm) as Node
// createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 把dom元素插入到父元素中,并且把旧的dom删除
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面
removeVnodes(parent, [oldVnode], 0, 0)
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
题外话:diff算法简介
传统diff算法
- 虚拟DOM中的Diff算法
-
传统算法
查找两颗树每一个节点的差异 - 会运行n1(dom1的节点数)*n2(dom2的节点数)次方去对比,找到差异的部分再去更新
snabbdom的diff算法优化
-
Snbbdom根据DOM的特点对传统的diff算法做了
优化
- DOM操作时候很少会跨级别操作节点
-
只比较
同级别
的节点
五、认识ref全家桶
文章列出包括(ref、isRef、shallowRef、triggerRef、customRef)的用法
1、ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个
.value
property,指向该内部值。
案例
我们这样操作是无法改变message 的值 应为message 不是响应式的无法被vue 跟踪要改成ref
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
let message: string = "我是message"
const changeMsg = () => {
message = "change msg"
</script>
<style>
</style>
改为ref
Ref TS对应的接口
interface Ref<T> {
value: T
}
注意被ref包装之后需要.value 来进行赋值
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import {ref,Ref} from 'vue'
let message:Ref<string> = ref("我是message")
const changeMsg = () => {
message.value = "change msg"
</script>
<style>
</style>
//--------------------------------ts两种方式
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
let message = ref<string | number>("我是message")
const changeMsg = () => {
message.value = "change msg"
</script>
<style>
</style>
2、isRef
判断是不是一个ref对象
import { ref, Ref,isRef } from 'vue'
let message: Ref<string | number> = ref("我是message")
let notRef:number = 123
const changeMsg = () => {
message.value = "change msg"
console.log(isRef(message)); //true
console.log(isRef(notRef)); //false
}
3、shallowRef
创建一个跟踪自身
.value
变化的 ref,但不会使其值也变成响应式的
例子1
修改其属性是非响应式的这样是不会改变的
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue';
let message = shallowRef({
name: "yzf"
const changeMsg = () => {
message.value.name = '666'
</script>
<style>
</style>
例子2
这样是可以被监听到的修改value,需要赋值整个对象
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue';
let message = shallowRef({
name: "yzf"
const changeMsg = () => {
message.value = { name: "666" }
</script>
<style>
</style>
可以看到这里的属性值已经被我们改变了(监听到了修改的value)
思考: 如果我们不想赋值整个对象,而只是想修改一个属性值,应该怎么做呢?
这里就需要引入一个新的ref概念 --- triggerRef
4、triggerRef
强制更新页面DOM
这样也是可以改变值的
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { Ref, shallowRef, triggerRef } from 'vue';
type Obj = {
name: string
let message: Ref<Obj> = shallowRef({
name: "yzf"
const changeMsg = () => {
message.value.name = '666'
triggerRef(message)
</script>
<style>
</style>
5、customRef
自定义ref
customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set
这块稍微有些复杂
<template>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { customRef } from 'vue'
function Myref<T>(value: T) {
return customRef((track, trigger) => {
return {
get() {
track()
return value
set(newVal: T) {
console.log('set');
value = newVal
trigger()
let message = Myref('yzf')
const changeMsg = () => {
message.value = '666'
</script>
<style>
</style>
六、认识reactive全家桶
用来绑定复杂的数据类型 例如 对象 数组
reactive 源码约束了我们的类型
他不允许绑定普通的数据类型 会给我们报错
import { reactive} from 'vue'
let person = reactive('yzf')
绑定普通的数据类型 我们可以使用ref,使用reactive 去修改值无须.value
1、reactive 基础用法
<script setup lang="ts">
import { reactive } from 'vue';
let person = reactive({
name:"yaozaofeng"
person.name = "YAOZAOFENG"
</script>
数组异步赋值问题
这样赋值页面是不会变化的应为会脱离响应式
let person = reactive<number[]>([])
setTimeout(() => {
person = [1, 2, 3]
console.log(person); // [1,2,3] 打印出来的值是已经变化了的
},1000)
解决方案1
使用push
import { reactive } from 'vue'
let person = reactive<number[]>([])
setTimeout(() => {
const arr = [1, 2, 3]
person.push(...arr)
console.log(person);
},1000)
方案2包裹一层对象
type Person = {
list?:Array<number>
let person = reactive<Person>({
list:[]
setTimeout(() => {
const arr = [1, 2, 3]
person.list = arr;
console.log(person);
},1000)
2、readonly
拷贝一份proxy对象将其设置为只读
import { reactive ,readonly } from 'vue'
const person = reactive({count:1})
const copy = readonly(person)
copy.count++ // 这里不能改变值,因为设置了只读状态
3、shallowReactive
只能对浅层的数据 如果是深层的数据只会改变值 不会改变视图
案例
<template>
<div>{{ state }}</div>
<button @click="change1">test1</button>
<button @click="change2">test2</button>
</template>
<script setup lang="ts">
import { shallowReactive } from 'vue'
const obj = {
a: 1,
first: {
b: 2,
second: {
const state = shallowReactive(obj)
function change1() {
state.a = 100
function change2() {
state.first.b = 200
state.first.second.c = 300
console.log(state);
</script>
<style>
</style>
七、 认识to系列的全家桶
toRef 、toRefs、toRaw
1、toRef
如果原始对象是非响应式的就不会更新视图 数据是会变的
<template>
<button @click="change">按钮</button>
{{state}}
</template>
<script setup lang="ts">
import { reactive, toRef } from 'vue'
const obj = {
foo: 1,
bar: 1
// 如果原始对象是响应式的是会更新视图并且改变数据的
// const obj = reactive({
// foo: 1,
// bar: 1
// })
const state = toRef(obj, 'bar')
// bar 转化为响应式对象
const change = () => {
state.value++
console.log(obj, state);
</script>
如果原始对象是响应式的是会更新视图并且改变数据的
// const obj = reactive({
// foo: 1,
// bar: 1
// })
2、toRefs
可以帮我们批量创建ref对象主要是方便我们解构使用
<template>
<div>foo---{{ foo }}</div>
<div>bar---{{ bar }}</div>
<button @click="change">按钮change</button>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const obj = reactive({
foo: 1,
bar: 1
let { foo, bar } = toRefs(obj)
foo.value++
console.log(foo, bar);
const change = () => {
foo.value++
bar.value--
</script>
toRefs能够帮助做到响应式 视图发生改变
3、toRaw
将响应式对象转化为普通对象
<template>
<div>foo---{{ obj.foo }}</div>
<div>bar---{{ obj.bar }}</div>
<button @click="change">按钮change</button>
</template>
<script setup lang="ts">
import { reactive, toRaw } from 'vue'
const obj = reactive({
foo: 1,
bar: 1
const state = toRaw(obj)
// 响应式对象转化为普通对象
const change = () => {
console.log(obj, state);
</script>
此时点击按钮视图不会发生变化
八、认识computed计算属性
计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
1、 函数形式
<template>
{{price}} --- {{m}} <!-- 500 --- $500 -->
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
let price = ref(0)//$0
let m = computed<string>(()=>{
return `$` + price.value
price.value = 500
</script>
2、对象形式
<template>
<div>{{ mul }}</div>
<button @click="mul = 100">click</button>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
let price = ref<number | string>(1)//$0
let mul = computed({
get: () => {
console.log('get')
return price.value
set: (value) => {
console.log('set')
price.value = 'set' + value
</script>
<style>
</style>
接下来使用一个购物车案例来学习巩固computed的知识
<template>
<table style="width:800px" border>
<thead>
<th>名称</th>
<th>数量</th>
<th>价格</th>
<th>操作</th>
</thead>
<tbody>
<tr :key="index" v-for="(item, index) in data">
<td align="center">{{ item.name }}</td>
<td align="center">
<button @click="addAndSub(item, false)">-</button>
{{ item.num }}
<button @click="addAndSub(item, true)">+</button>
<td align="center">{{ item.num * item.price }}</td>
<td align="center">
<button @click="del(index)">删除</button>
</tbody>
<tfoot>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">总价:{{ $total }}</td>
</tfoot>
</table>
</template>
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
type Shop = {
name: string,
num: number,
price: number
let $total = ref(0)
const data = reactive<Shop[]>([
name: "衣服",
num: 1,
price: 100
name: "裤子",
num: 2,
price: 200
name: "鞋子",
num: 3,
price: 300
const addAndSub = (item: Shop, type: boolean): void => {
if (item.num > 1 && !type) {
item.num--
if (item.num < 99 && type) {
item.num++
const del = (index: number) => {
data.splice(index, 1)
$total = computed<number>(() => {
return data.reduce((prev, next) => {
console.log(prev + (next.num * next.price))
return prev + (next.num * next.price)
}, 0)
</script>
<style>
</style>
这里实现一个简易的购物车,computed的使用主要体现再了总价的计算方法调用上,利用computed可以避免重复调用总价的计算方法。
拓展:reduce()方法的使用
reduce方法虽然参数比较多,有回调函数中的prev,cur,index,arr,还有reduce的第二个参数init,但是常用的也就prev(上一次回调的返回值)和cur(当前值)
arr.reduce(function(prev,cur,index,arr){
}, init);
arr 表示原数组;
prev 表示上一次调用回调时的返回值,或者初始值 init;
cur 表示当前正在处理的数组元素;
index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
init 表示初始值。
看上去是不是感觉很复杂?没关系,只是看起来而已,其实常用的参数只有两个:prev 和 cur。接下来我们跟着实例来看看具体用法吧~
reduce() 方法 的语法
arr.reduce(function(prev,cur,index,arr){ }, init);
arr.reduce(function(prev,cur,index,arr){
}, init);
arr 表示原数组;
prev 表示上一次调用回调时的返回值,或者初始值 init;
cur 表示当前正在处理的数组元素;
index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
init 表示初始值。
九、认识watch监听器
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用
watch第一个参数监听源
watch第二个参数回调函数cb(newVal,oldVal)
watch第三个参数一个options配置项是一个对象{undefined
immediate:true //是否立即调用一次
deep:true //是否开启深度监听
1、监听Ref
案例
import { ref, watch } from 'vue'
let message = ref({
nav:{
bar:{
name:""
watch(message, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
immediate:true,
deep:true
监听多个ref 注意监听多个的时候第一个参数变成数组啦
import { ref, watch ,reactive} from 'vue'
let message = ref('')
let message2 = ref('')
watch([message,message2], (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
})
2、监听Reactive
使用reactive监听深层对象开启和不开启deep 效果一样
案例1
<template>
<input v-model="message.nav.bar.name" type="text">
</template>
<script setup lang="ts">
import { ref, watch ,reactive} from 'vue'
let message = reactive({
nav:{
bar:{
name:""
watch(message, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
</script>
案例2 监听reactive 单一值
<template>
<input v-model="message.name" type="text">
<input v-model="message.name2" type="text">
</template>
import { ref, watch ,reactive} from 'vue'
let message = reactive({
name:"",
name2:""
watch(()=>message.name, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
})
十、认识watchEffect高级侦听器
watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
如果用到message 就只会监听message 就是用到几个监听几个 而且是非惰性 会默认调用一次
<template>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
watchEffect(() => {
console.log('message', message.value);
console.log('message2', message2.value);
</script>
清除副作用
就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖
<template>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
watchEffect((oninvalidate) => {
console.log('message', message.value);
console.log('message2', message2.value);
oninvalidate(() => {
console.log('before')
</script>
停止跟踪 watchEffect 返回一个函数 调用之后将停止更新
<template>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
<button @click="stopWatch">stopWatch</button>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
const stop = watchEffect((oninvalidate) => {
// console.log('message', message.value);
// console.log('message2', message2.value);
oninvalidate(() => {
console.log('before')
const stopWatch = () => stop()
</script>
更多的配置项
副作用刷新时机 flush 一般使用post
onTrigger 可以帮助我们调试 watchEffect
<template>
<input id="ipt" v-model="message" type="text" />
<input v-model="message2" type="text" />
<button @click="stopWatch">stopWatch</button>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
const stop = watchEffect((oninvalidate) => {
console.log('message', message.value);
oninvalidate(() => {
console.log('before')
flush: 'post',
onTrigger(e) {
debugger
const stopWatch = () => stop()
</script>
十一、认识组件&Vue3生命周期
组的生命周期
简单来说就是一个组件从创建 到 销毁的 过程 成为生命周期
在我们使用Vue3 组合式
API
是没有
beforeCreate 和 created 这两个生命周期的
onBeforeMount()
在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。
onMounted()
在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问
onBeforeUpdate()
数据更新时调用,发生在虚拟 DOM 打补丁之前。
updated()
DOM更新后,updated的方法即会调用。
onBeforeUnmounted()
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted()
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
十二、实操组件和认识 Less & Scoped
1、什么是less
Less (Leaner Style Sheets 的缩写) 是一门向后兼容的 CSS 扩展语言。这里呈现的是 Less 的官方文档(中文版),包含了 Less 语言以及利用 JavaScript 开发的用于将 Less 样式转换成 CSS 样式的 Less.js 工具。
因为 Less 和 CSS 非常像,因此很容易学习。而且 Less 仅对 CSS 语言增加了少许方便的扩展,这就是 Less 如此易学的原因之一。
官方文档 Less 快速入门 | Less.js 中文文档 - Less 中文网
在vite中使用less
npm install less less-loader -D 安装即可
在style标签注明即可
<style lang="less">
</style>
2、样式穿透问题: ::v-deep 和 /deep/ 以及 <<<
为什么需要 样式穿透 ?
比如element ui 的原生组件,它有自己的 CSS 样式,我们一般的外部修改一般不起作用,那么我们如何才能让它不会影响到内容的显示呢?这里我们就通过样式穿透来强制进行修改啦。 我们先在浏览器 F12 找到这个组件的类名。
样式穿透的 作用 :当我们使用 UI组件库,一般不能对其样式进行修改,但我们通过穿透可以进行修改。
用法
//如果使用的是css,可以用下面这种
外层容器 >>> 组件 {}
//但在css预处理器中用上面这种是无法生效的,类似在scss和less中,我们可以用下面这种。
外层容器 /deep/ 组件 {}
//但有些时候上面那种也没反应的时候,我们可以试一下下面这种
//我也不清楚为什么,但看比较多资料说,一般用下面这种各个方面会比较好。
外层容器 :: v-deep 组件 {}
补充上面:vue3.0的环境下,安装项目时选择了dart-sass,这个不支持/deep/和>>>的写法,只能用::v-deep,选择node-sass就不会(咋感觉还不是根源,不知道::v-deep的出处是哪里来的)
案例
//比如我们遇到需要对element ui 里面 el-avatar这个组件进行修改
el-avatar {
::v-deep .iconfont {
font-size: 24px;
color: #6b98b7;
background: #fff;
}
什么是scoped
实现组件的私有化, 当前style属性只属于当前模块.
在DOM结构中可以发现,vue通过在DOM结构以及css样式上加了唯一标记,达到样式私有化,不污染全局的作用
如图,这里class="a"所在元素设置的颜色不会改变class="b"的元素颜色。
十三、父子组件传参
父组件通过v-bind绑定数据,子组件通过defineProps接受传过来的值
案例
1、父传子
如下代码,子组件使用reactive创建一个list,然后使用v-bind:(可以缩写成:的形式)传参给Menu组件
<template>
<div class="layout">
<Menu :data="list" title="我是父组件的值"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue';
import Header from './Header/index.vue';
import Content from './Content/index.vue';
import { reactive } from 'vue'
const list = reactive<number[]>([1, 2, 3])
</script>
<style lang="less" scoped>
.layout {
display: flex;
height: 100%;
overflow: hidden;
&-right {
flex: 1;
display: flex;
flex-direction: column;
</style>
子组件接收父组件传过来的值
通过 defineProps 进行接收,这里的 defineProps 无需引入, 可以直接使用
如果我们使用的TypeScript
可以使用传递字面量类型的纯类型语法做为参数
如 这是TS特有的
<template>
<div class="menu">
菜单区域 {{ title }}
<div>{{ data }}</div>
</template>
<script setup lang="ts">
defineProps<{
title:string,
data:number[]
</script>
如果不是用TS
<script setup lang="ts">
defineProps({
title:{
default:"",
type:string
data:Array
</script>
TS 特有的默认值方式
withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值
<script setup lang="ts">
type Props = {
title?: string,
data?: number[]
withDefaults(defineProps<Props>(), { // 如果父组件有传data,则优先显示父组件传递的值,没有则使用这里的默认值
title: "默认值",
data: () => [1, 2, 3, 4, 5, 6]
</script>
2、子传父
子组件给父组件传参
是通过 defineEmits 派发一个事件
<template>
<div class="menu">
<button @click="clickTap">派发emit</button>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([6, 6, 6, 6, 6, 6, 6, 6, 6])
const emit = defineEmits(['on-click'])
const clickTap = () => {
emit('on-click', list)
</script>
我们在子组件绑定了一个click 事件 然后通过defineEmits 注册了一个自定义事件
点击click 触发 emit 去调用我们注册的事件 然后传递参数
父组件接受子组件的事件
<template>
<div class="layout">
<Menu @on-click="getList" title="我是父组件的值"></Menu>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue';
import { reactive } from 'vue'
const getList = (list:number[]) => {
console.log(list, '父组件接收子组件传过来的list') // 这里会打印出子组件传过来的list数组
</script>
我们从Menu 组件接受子组件派发的事件on-click 后面是我们自己定义的函数名称getList
会把参数返回过来
3、 子组件暴露给父组件的内部属性
通过defineExpose
我们从父组件获取子组件实例通过ref
<template>
<div class="layout">
<Menu ref="menus" @on-click="getList"></Menu>
</template>
<script setup lang="ts">
const menus = ref(null)
const getList = (list:number[]) => {
// console.log(list, '父组件接收子组件传过来的list')
console.log(menu.value);
</script>
然后打印menus.value 发现target中没有任何属性
这时候父组件想要读到子组件的属性可以通过 defineExpose暴露
<template>
<div class="menu">
<button @click="clickTap">派发emit</button>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([6, 6, 6, 6, 6, 6, 6, 6, 6])
const emit = defineEmits(['on-click'])
const clickTap = () => {
emit('on-click', list)
defineExpose({
</script>
现在我们再次点击button按钮就能够打印出子组件暴露给父组件的list了
补充:
这里menu.value获取的是一个proxy对象,我们需要获取其值,有两种方法:
第一种获取target值的方式:
通过vue中的响应式对象课用 toRaw 方法获取原始对象
//第一种获取target值的方式,通过vue中的响应式对象可使用toRaw()方法获取原始对象
import { toRaw } from '@vue/reactivity'
var list = toRaw(store.state.menuList)
第二种获取target值的方式,通过json序列化之后可获取值
JSON.parse(JSON.stringify(menu.value))
十四、使用全局组件、局部组件、递归组件
1、配置全局组件
例如组件使用频率非常高(table,Input,button、等等)这些组件 几乎每个页面都在使用便可以封装成全局组件
案例------我这儿封装一个Card组件想在任何地方去使用
<template>
<div class="card">
<div class="card-header">
<div>标题</div>
<div>副标题</div>
<div v-if='content' class="card-content">
{{content}}
</template>
<script setup lang="ts">
type Props = {
content:string
defineProps<Props>()
</script>
<style scoped lang='less'>
@border:#ccc;
.card{
width: 300px;
border: 1px solid @border;
border-radius: 3px;
&:hover{
box-shadow:0 0 10px @border;
&-content{
padding: 10px;
&-header{
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid @border;
</style>
使用方法
在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用用
其次调用 component 第一个参数组件名称 第二个参数组件实例
import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset.less'
import Card from './components/Card/index.vue' // 这是我们引入的案例组件
createApp(App).component('Card', Card).mount('#app') // 在mount前面注册组件实例
使用方法
直接在其他vue页面 立即使用即可 无需引入
<template>
<Card></Card>
</template>
2、配置局部组件
<template>
<div class="layout">
<Menu ref="menu" @on-click="getList" title="我是父组件的值"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import Menu from './Menu/index.vue';
import Header from './Header/index.vue';
import Content from './Content/index.vue';
</script>
就是在一个组件内(A) 通过import 去引入别的组件(B) 称之为局部组件
应为 B 组件只能在A组件内使用 所以是局部组件
如果 C 组件想用B组件 就需要 C 组件 import 引入 B 组件
3、配置递归组件
原理跟我们写js递归是一样的 自己调用自己 通过一个条件来结束递归 否则导致内存泄漏
案例:递归树
父组件配置数据结构 数组对象格式 传给子组件
<script setup lang="ts">
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
const data = reactive<TreeList[]>([
name: "no.1",
children: [
name: "no.1-1",
children: [
name: "no.1-1-1",
name: "no.2",
children: [
name: "no.2-1",
name: "no.3",
name: "no.4",
children: []
</script>
子组件接收值
这里的 type TreeList 和上面的 type TreeList 可以提取出来作为公用
<script setup lang="ts">
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
type Props<T> = {
data?: T[] | [];
defineProps<Props<TreeList>>();
</script>
template
TreeItem 其实就是当前组件 通过import 把自身又引入了一遍 如果他没有 children 就结束递归
<template>
<div style="margin-left: 10px;">
<div @click.stop="clickItem(item)" :key="index" v-for="(item, index) in data">
{{ item.name }}
<TreeItem @on-click="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
</template>
附完整的代码
这里增加了树每个节点的点击事件,返回该节点的值,通过父子组件传值的方式,将子组件点击的该节点值传递给父节点
Tree.vue
<template>
<div style="margin-left: 10px;">
<div @click.stop="clickItem(item)" :key="index" v-for="(item, index) in data">
{{ item.name }}
<TreeItem @on-click="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
</template>
<script setup lang='ts'>
import { reactive } from 'vue'
type TreeList = {
name?: string,
icon?: string,
children?: TreeList[] | []
type Props = {
data?: TreeList[]
defineProps<Props>()
const emit = defineEmits(['on-click'])
const clickItem = (item: TreeList) => {
// console.log(item, '子组件的item')
emit('on-click', item)
</script>
<script lang="ts">
export default {
name: 'TreeItem'
</script>
<style scoped lang='less'>
</style>
Card.vue
<template>
<div class="card">
<div class="card-header">
<div>主标题</div>
<div>副标题</div>
<div class="card-content" v-if="content">
{{content}}
</template>
<script setup lang="ts">
type Props = {
content?:string
defineProps<Props>()
</script>
<style scoped lang='less'>
@border:#ccc;
.card{
width: 100%;
border: 1px solid @border;
border-radius: 3px;
&:hover{
box-shadow:0 0 10px @border;
&-content{
padding: 10px;
&-header{
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid @border;
</style>
Menu.vue
<template>
<div class="menu">
<Tree @on-click="getItem" :data="data"></Tree>
</template>
<script setup lang="ts">
import Tree from '../../components/Tree/index.vue'
import { reactive } from 'vue'
type TreeList = {
name?: string,
icon?: string,
children?: TreeList[] | []
const getItem = (item: TreeList) => {
console.log(item, '父组件的item')
const data = reactive<TreeList[]>([
name: "no.1",
children: [
name: "no.1-1",
children: [
name: "no.1-1-1",
name: "no.2",
children: [
name: "no.2-1",
name: "no.3",
name: "no.4",
children: []
</script>
<style lang="less" scoped>
.menu {
width: 200px;
border-right: 1px solid #ccc;
</style>
十五、简单实现动态组件
第一步,创建A、B、C三个组件,在另外的组件中引入这三个组件;
第二步,创建Tabs类型数据;
第三步,利用Tabs类型创建data数组,赋予name和comName属性,用来记录各个组件名称;
第四步,声明Com,使用Pick将Tabs中的comName提取出来使用;
第五步,声明current,将data第一个属性值作为默认值,并创建component组件展示;
第六步,添加switchCom点击事件,实现点击tab切换功能。
<template>
<div class="content">
<div class="tab">
<div @click="switchCom(item)" :key="index" v-for="(item, index) in data">{{ item.name }}</div>
<component :is="current.comName"></component>
</template>
<script setup lang="ts">
import { reactive, markRaw } from 'vue';
import A from './A.vue'
import B from './B.vue'
import C from './C.vue'
type Tabs = {
name: string,
comName: any
type Com = Pick<Tabs, 'comName'>
const data = reactive<Tabs[]>([
name: "我是A组件",
comName: markRaw(A)
name: "我是B组件",
comName: markRaw(B)
name: "我是C组件",
comName: markRaw(C)
let current = reactive<Com>({
comName: data[0].comName
const switchCom = (item: Tabs) => {
current.comName = item.comName
</script>
<style lang="less" scoped>
.tab {
display: flex;
.active {
background: skyblue;
color: #fff;
div {
border: 1px solid #ccc;
margin: 20px 0 0 20px;
padding: 5px;
box-sizing: border-box;
div:hover {
cursor: pointer;
</style>
toRaw 和 markRaw
Vue3.0给我们提供的这两个方法,toRaw方法是把被reactive或readonly后的Proxy对象转换为原来的target对象,而markRaw则直接让target不能被reactive或readonly。
上面代码使用markRaw是因为组件进行了代理,不需要Tabs再次进行proxy代理了,消除警告。
十六、认识插槽全家桶
插槽 就是子组件中的提供给父组件使用的一个 占位符 ,用<slot></slot> 表示,父组件可以在这个 占位符 中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。
1、匿名插槽
- 在子组件放置一个插槽
<template>
<slot></slot>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<template>
<Dialog>
<template v-slot>
<div>666</div>
</template>
</Dialog>
</template>
2、具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<template>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</template>
父组件使用需对应名称
注:插槽简写 v-slot:header 可以简写成 #header
<template>
<div class="content">
<Dialog>
<template #header>
</template>
<template v-slot>
我被插入了中间
</template>
<template #footer>
</template>
</Dialog>
</template>
十六、认识插槽全家桶
插槽 就是子组件中的提供给父组件使用的一个 占位符 ,用<slot></slot> 表示,父组件可以在这个 占位符 中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。
1、匿名插槽
- 在子组件放置一个插槽
<template>
<slot></slot>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<template>
<Dialog>
<template v-slot>
<div>666</div>
</template>
</Dialog>
</template>
2、具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<template>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</template>
父组件使用需对应名称
注:插槽简写 v-slot:header 可以简写成 #header
<template>
<div class="content">
<Dialog>
<template #header>
</template>
<template v-slot>
我被插入了中间
</template>
<template #footer>
</template>
</Dialog>
</template>
3、作用域插槽
在子组件动态绑定参数 派发给父组件的slot去使用
<template>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<div v-for="(item, index) in data">
<slot :index="index" :data="item"></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</template>
<script setup lang='ts'>
import { reactive } from 'vue'
type names = {
name: string,
age: number
const data = reactive<names[]>([{
name: "被插入le",
age: 201
name: "被插入le",
age: 202
name: "被插入le",
age: 203
name: "被插入le",
age: 204
</script>
父组件接收子组件传过来的slot, 通过解构的方式进行取值
<template>
<div class="content">
<Dialog>
<template #header>
<div>插入上面</div>
</template>
<template #default="{ data, index }">
<div>{{ data.name }} --- {{ data.age }} --- {{ index }}</div>
</template>
<template #footer>
<div>插入下面</div>
</template>
</Dialog>
</template>
4、动态插槽
<template>
<div class="content">
<Dialog>
<template #[name]>
<div>不知道自己会被插到哪里,根据name的值来确定对应的slot位置</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Dialog from '../../components/Dialog/index.vue'
let name = ref("footer")
</script>
<style lang="less" scoped>
</style>
const name = ref("footer")
通过这个确定会被插入#footer名的slot插槽内
十八、Teleport 传送组件
Teleport
Vue 3.0新特性之一。
Teleport
是一种能够将我们的模板渲染至指定
DOM
节点,不受父级
style
、
v-show
等属性影响,但
data
、
prop
数据依旧能够共用的技术;类似于
React
的
Portal
。
主要解决的问题 因为
Teleport
节点挂载在其他指定的
DOM
节点下,完全不受父级
style
样式影响,常用于一些全局控件上面,例如Loading空控件。
使用方法
通过 to 属性 插入指定元素位置 to="body" 便可以将
Teleport
内容传送到指定位置
<Teleport to="body">
<Loading></Loading>
</Teleport>
也可以自定义传送位置 支持 class、 id等 选择器
<div id="app"></div>
<div class="modal"></div>
<Teleport to=".modal">
<Loading></Loading>
</Teleport>
也可以使用多个
<Teleport to=".modal1">
<Loading></Loading>
</Teleport>
<Teleport to=".modal2">
<Loading></Loading>
</Teleport>
十九、vue-router 之 keep-alive 缓存组件
内置组件keep-alive
keep-alive
是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到
keep-alive
组件。
开启keep-alive 生命周期 的变化
- 初次进入时: onMounted> onActivated
-
退出后触发
deactivated
- 再次进入:
- 只会触发 onActivated
- 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中
<keep-alive>
<component>
<!-- 该组件将被缓存! -->
</component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="bool === true"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
1、props: include 和 exclude
<keep-alive :include="" :exclude="" :max=""></keep-alive>
<keep-alive include="a">
<component>
<!-- name 为 a 的组件将被缓存! -->
</component>
</keep-alive>可以保留它的状态或避免重新渲染
<keep-alive exclude="a">
<component>
<!-- 除了 name 为 a 的组件都将被缓存! -->
</component>
</keep-alive>可以保留它的状态或避免重新渲染
include & exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔 字符串 、正则表达式或一个数组来表示
2、 max 最多可以缓存多少组件的实例
切记这个max属性必须大于0才能有效。。至少有一个要被缓存
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
二十、transition动画组件
transition组件用于控制组件、元素的显示消失切换效果
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
自定义 transition 过度效果,你需要对
transition
组件的
name
属性自定义。并在css中写入对应的样式
使用步骤
1、使用
<transition>
组件包裹需要切换得 组件/元素
<transition name="animation">
<div v-show="isShow" class='box'>
切换显示的元素 / 组件
<button>按钮</button>
</transition>
2、定义过渡的类名
过渡 class
在进入/离开的过渡中,会有 6 个 class 切换。
1、v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
2、v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
3、v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
4、v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
5、v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
6、v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
<button @click='flag = !flag'>切换</button>
<transition name='fade'>
<div v-if='flag' class="box"></div>
</transition>
CSS
//开始过渡
.fade-enter-from{
background:red;
width:0px;
height:0px;
transform:rotate(360deg)
//开始过渡了
.fade-enter-active{
transition: all 2.5s linear;
//过渡完成
.fade-enter-to{
background:yellow;
width:200px;
height:200px;
//离开的过渡
.fade-leave-from{
width:200px;
height:200px;
transform:rotate(360deg)
//离开中过渡
.fade-leave-active{
transition: all 1s linear;
//离开完成
.fade-leave-to{
width:0px;
height:0px;
}
3、自定义过渡 class 类名
trasnsition props
enter-from-class enter-active-class enter-to-class leave-from-class leave-active-class leave-to-class 自定义过度时间 单位毫秒
你也可以分别指定进入和离开的持续时间:
<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
4、通过自定义class 结合css动画库animate css
安装库 npm install animate.css
引入 import 'animate.css'
使用方法
官方文档 Animate.css
<transition
leave-active-class="animate__animated animate__bounceInLeft"
enter-active-class="animate__animated animate__bounceInRight"
<div v-if="flag" class="box"></div>
</transition>
transition 生命周期 8 个
@before-enter="beforeEnter" //对应enter-from
@enter="enter"//对应enter-active
@after-enter="afterEnter"//对应enter-to
@enter-cancelled="enterCancelled"//显示过度打断
@before-leave="beforeLeave"//对应leave-from
@leave="leave"//对应enter-active
@after-leave="afterLeave"//对应leave-to
@leave-cancelled="leaveCancelled"//离开过度打断
当只用 JavaScript 过渡的时候,在
enter
和
leave
钩子中必须使用
done
进行回调
结合gsap 动画库使用 GreenSock
const beforeEnter = (el: Element) => {
console.log('进入之前from', el);
const Enter = (el: Element,done:Function) => {
console.log('过度曲线');
setTimeout(()=>{
done()
},3000)
const AfterEnter = (el: Element) => {
console.log('to');
}
5、appear
通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态
appear-active-class=""
appear-from-class=""
appear-to-class=""
appear
二十一、transition-group过渡列表
1、在 Vue 3 中,我们有了
片段的支持
,因此组件不再
需要
根节点。所以,
<transition-group>
不再默认渲染根节点。
- 单个节点
- 多个节点,每次只渲染一个
- 那么怎么同时渲染整个列表,比如使用 v-for?在这种场景下,我们会使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:
- 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag attribute 指定渲染一个元素。
- 过渡模式不可用,因为我们不再相互切换特有的元素。
- 内部元素总是需要提供唯一的 key attribute 值。
- CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
<transition-group>
<div style="margin: 10px;" :key="item" v-for="item in list">{{ item }</div>
</transition-group>
const list = reactive<number[]>([1, 2, 4, 5, 6, 7, 8, 9])
const Push = () => {
list.push(123)
const Pop = () => {
list.pop()
}
2、列表的移动过渡案例
<transition-group>
组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的 v-move 类就可以使用这个新功能,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name attribute 来自定义,也可以通过 move-class attribute 手动设置
下面来实现一个酷炫的代码案例:
<template>
<div style="padding:30px;">
<button style="width:100px;height:70px;margin-bottom:20px;" @click="random">random</button>
<transition-group move-class="move" class="wraps" tag="div">
<div class="items" :key="item.id" v-for="item in list">{{ item.number }}</div>
</transition-group>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import _ from 'lodash'
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 = () => {
list.value = _.shuffle(list.value)
setInterval(random,5000);
</script>
<style lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(50px * 10 + 9px);
.items {
width: 50px;
height: 50px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
.move {
transition: all 1s;
</style>
3、状态过渡
数字变化案例, 这里引入 gsap 库帮助我们实现动效
<template>
<input step="20" v-model="num.current" type="number" />
<div>{{ num.tweenedNumber.toFixed(0) }}</div>
</template>
<script setup lang='ts'>
import { reactive, watch } from 'vue'
import gsap from 'gsap'
const num = reactive({
tweenedNumber: 0,
current:0
watch(()=>num.current, (newVal) => {
gsap.to(num, {
duration: 1,
tweenedNumber: newVal