目录
一、vue介绍
二、教你创建新项目
三、介绍Vue3 模板语法、插入指令
四、介绍虚拟Dom和Diff算法
五、认识ref全家桶
六、认识reactive全家桶
七、认识to系列的全家桶
八、认识computed计算属性
九、认识watch监听器
十、认识watchEffect高级侦听器
十一、认识组件&Vue3生命周期
十二、实操组件和认识 Less & Scoped
十三、父子组件传参
十四、使用全局组件、局部组件、递归组件
十五、简单实现动态组件
十六、认识插槽全家桶
十八、Teleport 传送组件
十九、vue-router 之 keep-alive 缓存组件
二十、 更新中...
一、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 允许我们支持多个根节点
同时支持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确定一下版本)
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>
块 (不包括
该脚本将作为 ES Module 来执行。
其默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值。
<script setup> 每个 *.vue 文件最多可同时包含一个 <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>
编辑切换为居中
添加图片注释,不超过 140 字(可选)
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>
</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;
}
.h {
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;
}
.h {
height: 300px;
border: 1px solid #ccc;
}
</style>
四、虚拟Dom和Diff算法
为什么要学习源码
1.可以提升自己学习更优秀的API设计和代码逻辑
2.面试的时候也会经常问源码相关的东西
3.更快的掌握vue和遇到问题可以定位
介绍虚拟Dom
虚拟DOM就是通过JS来生成一个AST节点树
编辑切换为居中
添加图片注释,不超过 140 字(可选)
为什么要有虚拟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
编辑切换为居中
添加图片注释,不超过 140 字(可选)
上述情况: 示例1是创建一个DOMB然后替换掉DOMA; 示例2去创建虚拟DOM+DIFF算法比对发现DOMB跟DOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA; 可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+DIFF算啊对比 所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的
举例:当DOM树里面的某个子节点的内容变更时:
编辑切换为居中
添加图片注释,不超过 140 字(可选)
当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例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的节点数)次方去对比,找到差异的部分再去更新
编辑切换为居中
添加图片注释,不超过 140 字(可选)
snabbdom的diff算法优化
- Snbbdom根据DOM的特点对传统的diff算法做了优化
- DOM操作时候很少会跨级别操作节点
- 只比较同级别的节点
五、认识ref全家桶
文章列出包括(ref、isRef、shallowRef、triggerRef、customRef)的用法
1、ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
案例
我们这样操作是无法改变message 的值 应为message 不是响应式的无法被vue 跟踪要改成ref
<template>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue';
let message = shallowRef({
name: "yzf"
})
const changeMsg = () => {
message.value = { name: "666" }
}
</script>
<style>
</style>
编辑
添加图片注释,不超过 140 字(可选)
可以看到这里的属性值已经被我们改变了(监听到了修改的value)
思考: 如果我们不想赋值整个对象,而只是想修改一个属性值,应该怎么做呢?
这里就需要引入一个新的ref概念 --- triggerRef
4、triggerRef
强制更新页面DOM
这样也是可以改变值的
<template>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</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 源码约束了我们的类型
编辑切换为居中
添加图片注释,不超过 140 字(可选)
他不允许绑定普通的数据类型 会给我们报错
import { reactive} from 'vue'
let person = reactive('yzf')
编辑
添加图片注释,不超过 140 字(可选)
绑定普通的数据类型 我们可以使用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>
<div>{{ state }}</div>
<button @click="change1">test1</button>
<button @click="change2">test2</button>
</div>
</template>
<script setup lang="ts">
import { shallowReactive } from 'vue'
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}
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>
<div>
<button @click="change">按钮</button>
{{state}}
</div>
</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>
<div>foo---{{ foo }}</div>
<div>bar---{{ bar }}</div>
<div>
<button @click="change">按钮change</button>
</div>
</div>
</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能够帮助做到响应式 视图发生改变
编辑
添加图片注释,不超过 140 字(可选)
3、toRaw
将响应式对象转化为普通对象
<template>
<div>
<div>foo---{{ obj.foo }}</div>
<div>bar---{{ obj.bar }}</div>
<div>
<button @click="change">按钮change</button>
</div>
</div>
</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>
<div>
{{price}} --- {{m}} <!-- 500 --- $500 -->
</div>
</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>
<div>
<table style="width:800px" border>
<thead>
<tr>
<th>名称</th>
<th>数量</th>
<th>价格</th>
<th>操作</th>
</tr>
</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>
<td align="center">{{ item.num * item.price }}</td>
<td align="center">
<button @click="del(index)">删除</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">总价:{{ $total }}</td>
</tr>
</tfoot>
</table>
</div>
</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>
编辑切换为居中
添加图片注释,不超过 140 字(可选)
这里实现一个简易的购物车,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。接下来我们跟着实例来看看具体用法吧~
编辑切换为居中
添加图片注释,不超过 140 字(可选)
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>
<div>
<input v-model="message.nav.bar.name" type="text">
</div>
</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>
<div>
<input v-model="message.name" type="text">
<input v-model="message.name2" type="text">
</div>
</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>
<div>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
</div>
</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>
<div>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
</div>
</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>
<div>
<input v-model="message" type="text" />
<input v-model="message2" type="text" />
<button @click="stopWatch">stopWatch</button>
</div>
</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
编辑切换为居中
添加图片注释,不超过 140 字(可选)
onTrigger 可以帮助我们调试 watchEffect
<template>
<div>
<input id="ipt" v-model="message" type="text" />
<input v-model="message2" type="text" />
<button @click="stopWatch">stopWatch</button>
</div>
</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样式上加了唯一标记,达到样式私有化,不污染全局的作用
编辑切换为居中
添加图片注释,不超过 140 字(可选)
如图,这里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>
</div>
</div>
</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>
</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">
menu
<button @click="clickTap">派发emit</button>
</div>
</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>
</div>
</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>
</div>
</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">
menu
<button @click="clickTap">派发emit</button>
</div>
</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({
list
})
</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>
<div v-if='content' class="card-content">
{{content}}
</div>
</div>
</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>
</div>
</div>
</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>
</div>
</div>
</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>
</div>
</div>
</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>
<div class="card-content" v-if="content">
{{content}}
</div>
</div>
</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>
</div>
</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>
</div>
<component :is="current.comName"></component>
</div>
</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代理了,消除警告。
十六、认识插槽全家桶
插槽就是子组件中的提供给父组件使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。
1、匿名插槽
- 在子组件放置一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<template>
<Dialog>
<template v-slot>
<div>666</div>
</template>
</Dialog>
</template>
2、具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<template>
<div>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件使用需对应名称
注:插槽简写 v-slot:header 可以简写成 #header
<template>
<div class="content">
<Dialog>
<template #header>
<div>
插入上面
</div>
</template>
<template v-slot>
<div>
我被插入了中间
</div>
</template>
<template #footer>
<div>
插入下面
</div>
</template>
</Dialog>
</div>
</template>
编辑
添加图片注释,不超过 140 字(可选)
十六、认识插槽全家桶
插槽就是子组件中的提供给父组件使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。
1、匿名插槽
- 在子组件放置一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<template>
<Dialog>
<template v-slot>
<div>666</div>
</template>
</Dialog>
</template>
2、具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<template>
<div>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件使用需对应名称
注:插槽简写 v-slot:header 可以简写成 #header
<template>
<div class="content">
<Dialog>
<template #header>
<div>
插入上面
</div>
</template>
<template v-slot>
<div>
我被插入了中间
</div>
</template>
<template #footer>
<div>
插入下面
</div>
</template>
</Dialog>
</div>
</template>
编辑
添加图片注释,不超过 140 字(可选)
3、作用域插槽
在子组件动态绑定参数 派发给父组件的slot去使用
<template>
<div>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<div v-for="(item, index) in data">
<slot :index="index" :data="item"></slot>
</div>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</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>
</div>
</template>
4、动态插槽
<template>
<div class="content">
<Dialog>
<template #[name]>
<div>不知道自己会被插到哪里,根据name的值来确定对应的slot位置</div>
</template>
</Dialog>
</div>
</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>
学习资料来源: