Vue3学习笔记(coderwhy)
Vue基础语法
MVVM模型
data属性
data属性是传入一个函数,并且该函数需要返回一个对象:
在Vue2.x的时候,也可以传入一个对象(虽然官方推荐是一个函数);
在Vue3.x的时候,必须传入一个函数,否则就会直接在浏览器中报错;
◼ data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理:
所以我们在template或者app中通过 { {counter}} 访问counter,可以从对象中获取到数据;
所以我们修改counter的值时,app中的 { {counter}}也会发生改变;
◼ 具体这种响应式的原理,我们后面会有专门的篇幅来讲解。
面试题:为什么data要是一个函数?
methods属性
◼ methods属性是一个对象,通常我们会在这个对象中定义很多的方法:
这些方法可以被绑定到模板中;
在该方法中,我们可以使用this关键字来直接访问到data中返回的对象的属性;
◼ 对于有经验的同学,在这里我提一个问题,官方文档有这么一段描述:
问题一:为什么不能使用箭头函数(官方文档有给出解释)?
问题二:不使用箭头函数的情况下,this到底指向的是什么?(可以作为一道面试题)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TzNU8ZLp-1689141095301)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230609150529252.png)]
问题一:不能使用箭头函数?
◼ 我们在methods中要使用data返回对象中的数据:
那么这个this是必须有值的,并且应该可以通过this获取到data返回对象中的数据。
◼ 那么我们这个this能不能是window呢?
不可以是window,因为window中我们无法获取到data返回对象中的数据;
但是如果我们使用箭头函数,那么这个this就会是window了;
◼ 为什么是window呢?
这里涉及到箭头函数使用this的查找规则,它会在自己的上层作用于中来查找this;
最终刚好找到的是script作用于中的this,所以就是window;
问题二:this到底指向什么?
◼ 事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过bind绑定了this:
Vue3基本指令
Mustache 语法
◼ 如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。
并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中;
当data中的数据发生改变时,对应的内容也会发生更新。
当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。
◼ 另外这种用法是错误的:
-
动态显示文本信息
<template id="my-app"> <h2>{ {msg}}</h2> <!-- 也可是表达式 --> <h2>{ {count + 101}}</h2> <!-- 当然也可以复杂的方法调用 --> <h2>{ {msg.replace('world', 'Vue3')}}</h2> <!-- 也可以是个函数 --> <h2>{ {demo()}}</h2> <!-- 也可以使用计算属性或者三元运算符 --> </template>
v-bind指令
- 有时候我们希望除了显示的文本信息可以动态显示之外,我们也希望某些属性可以动态的绑定,比如 img 的 src 属性
- 绑定属性我们可以使用
v-bind
,简写形式为:
基本使用与简写
-
下面我们来看一下基本的使用方法,一般我们都是使用简写
<a v-bind:href="path">跳转1</a> <br /> <!-- 或者简写 --> <a :href="path">跳转2</a> <script> const App = { template: '#my-app', data() { return { path: 'https://www.baidu.com' } } } Vue.createApp(App).mount('#app') </script>
三元运算写法
-
根据一个条件来动态决定是否加这个类名,达到简单的样式切换效果
<div class="box" :class=" falg?'active':'' "></div> <script> const App = { template: '#my-app', data() { return { falg: true } }, methods: { }, } Vue.createApp(App).mount('#app') </script>
动态绑定class - 对象写法
-
动态绑定 class 类名的对象写法,可以利用对象写法来达成批量加入类名的方法
<!-- { className: boolean },后面的布尔值决定是否使用该类名 --> <div :class="{ 'box':true, 'active': true}"></div> <!-- 这个 boolean 也可以是一个表达式或者一个变量 --> <div :class="{ box:true, active: flag}"></div> <!-- 当然可以选择将这个对象提取出去,使用一个变量代替 --> <div :class="objName"></div> <script> const App = { template: '#my-app', data() { return { objName:{ box: true, active: true}" } } } Vue.createApp(App).mount('#app') </script>
动态绑定class - 数组写法
-
这种方式绑定多个类名,我们就比较常用
<div :class="['aaa','bbb', className, isActive? 'active': '', { active: isActive }]"></div> <!-- 当然数组里面还可以嵌套三元运算符与对象写法 --> <script> const App = { template: '#my-app', data() { return { className: 'ccc' } } } Vue.createApp(App).mount('#app') </script>
动态绑定 style - 对象写法
-
style 有时候层级比较高,我们有时候可能会需要这种权重比较高的属性来决定样式
<!-- 2.1.动态绑定style, 在后面跟上 对象类型 (重要)--> <h2 v-bind:style="{ color: fontColor, fontSize: fontSize + 'px' }">哈哈哈哈</h2> <!-- 2.2.动态的绑定属性, 这个属性是一个对象 --> <h2 :style="objStyle">呵呵呵呵</h2> <script> const App = { template: '#my-app', data: function() { return { fontColor: "blue", fontSize: 30, objStyle: { fontSize: '50px', color: "green" } } }, } Vue.createApp(App).mount('#app') </script>
动态绑定 style - 数组写法
-
可以使用数组嵌套多个对象,实现多个属性的绑定
<div :style="[styleObj1, styleObj2]"></div> <script> const App = { template: '#my-app', data() { return { styleObj1: { color: '#f40', 'background-color': 'pink' // 多个单词的使用 - 链接需要添加 '' / 也可以改成驼峰式命名 }, styleObj2: { backgroundImage: 'url(./abc.img)' } } }, } Vue.createApp(App).mount('#app') </script>
动态绑定属性名
-
有时候我们的属性名称可能也不是固定的,比如 :class 的这个 class 属性名称我们也需要更换
<!-- 语法为 :[] = "" --> <div :[key]="value"></div> <script> const App = { template: '#my-app', data() { return { key:'aaa', value:'xxx' } }, } Vue.createApp(App).mount('#app') </script>
-
如果我们需要给一个元素批量绑定属性的话,比如:
,就可以使用 v-bind=obj 的方法,一般就会经常用于我们封装的高阶组件,如下:<div v-bind="obj"></div> <script> const App = { template: '#my-app', data() { return { obj:{ name:'aaa', age:18, address:'changsha' } } }, } Vue.createApp(App).mount('#app') </script>
v-on 绑定事件
- 前面我们绑定了元素的
内容和属性
,在前端开发中另外一个非常重要的特性就是交互 - 在这个时候我们需要监听点击、拖拽、键盘等事件,可以使用 v-on 来监听事件
- 完整写法:v-on
- 简写:
@
- 参数:event
写法与简写
-
一般情况下也是使用简写方式
<!-- 完整写法 --> <button v-on:click="btn1Click">按钮1</button> <!-- 简写 --> <button @click="btn1Click">按钮1</button> <script> const App = { template: '#my-app', data() { return { } }, methods: { btn1Click() { console.log('btn1被点击了') } }, } Vue.createApp(App).mount('#app') </script>
表达式
-
除了直接绑定方法之外,也可以绑定表达式,来省去方法的这一步步骤
<button @click="count++">按钮1</button> <script> const App = { template: '#my-app', data() { return { count: 0 } } } Vue.createApp(App).mount('#app') </script>
同时绑定多个事件
-
如果你希望一次绑定多个事件,可以尝试一下以下的写法
<div v-on="{ click: btn1Click, mousemove: move }"></div> <!-- 当然 v-on 也可以用简写 @ 代替 --> <script> const App = { template: '#my-app', data() { return { } }, methods: { btn1Click() { console.log('btn1被点击了') }, move(){ console.log('开始移动') } }, } Vue.createApp(App).mount('#app') </script>
v-on 参数传递
-
默认传递一个 event 对象,也就是元素本身,绑定事件时不需要传递参数,方法本身会生成一个 event 事件,如下
<button @click="btnClick">按钮1</button> <script> const App = { template: '#my-app', data() { return { msg: 'hello world' } }, methods: { btnClick(event) { console.log(event) } }, } Vue.createApp(App).mount('#app') </script>
-
如果我们的监听点击事件时还需要传递一些其他参数呢?这时候怎么接收传递的参数和传递参数之后怎么接收 event 事件,如下
<!-- 传递自定义参数 age是data中的变量--> <button @click="btn2Click(1,age)">按钮2</button> <script> const App = { template: '#my-app', data() { return { age:12 } }, methods: { btn2Click(val1, val2) { console.log(val1, val2) } }, } Vue.createApp(App).mount('#app') </script> <!-- 传递自定义参数 + evnet,我们需要 给他 event 添加一个 $ 符号 --> <button @click="btn3Click(1, age, $event)">按钮3</button> <script> const App = { template: '#my-app', data() { return { age:19 } }, methods: { btn3Click(val1, val2, e) { console.log(val1, val2, e) } }, } Vue.createApp(App).mount('#app') </script>
v-on 的修饰符
-
修饰符比较多,大家看图即可:
(了解)v-memo
//用于性能优化
<div id="app">
<div v-memo="[name]">//只有改变的是name的时候才渲染,如果改变的是age,则不会重新渲染
<h2>姓名: {
{ name }}</h2>
<h2>年龄: {
{ age }}</h2>
<h2>身高: {
{ height }}</h2>
</div>
<button @click="updateInfo">改变信息</button>
</div>
<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data: function() {
return {
name: "why",
age: 18,
height: 1.88
}
},
methods: {
updateInfo: function() {
// this.name = "kobe"
this.age = 20
}
}
})
// 2.挂载app
app.mount("#app")
</script>
(了解)v-once 指令
v-once 用于指定元素或组件只渲染一次
- 当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过
- 该指令在特地场景下可以用于性能优化
<template id="my-app">
<h2>{
{count}}</h2>
<!-- 给一个元素加上 v-once 指令后表示此处模板只会渲染一次,哪怕后续数据在改动也不会执行 -->
<!-- 包括其内部的元素或者组件 -->
<div v-once>
<h2>{
{count}}</h2>
</div>
<!-- 加上 once 后缀此事件表示只可执行一次 -->
<!-- <button @click.once="increase">自增一次</button> -->
<button @click="increase">自增一次</button>
</template>
(了解)v-text 指令
用于更新元素的 textContent
<template id="my-app">
<h2 v-text="msg">aaa</h2>//msg将会替换掉aaa
<h2>{
{msg}}</h2>
</template>
(了解)v-html 指令
默认情况下,如果我们展示的内容本事是 html 的,那么 Vue并不会对其进行特殊的解析
- 如果希望这个内容被 Vue 可以解析出来,那么可以使用 v-html 来展示
<template id="my-app">
<!-- 如果不使用 v-html 指令会将我们写的html结构作为字符串展示 -->
<div>{
{msg}}</div>
<!-- 使用 v-html 指令即可解析 在div里会生成span标签的哈哈哈-->
<div v-html="msg"></div>
</template>
data(){
return {
msg:`<span style="color:red;font-size:18px;">哈哈哈哈</span>`
}
}
(了解)v-pre 指令
用于跳过元素和他的子元素的编译过程,显示原始的 Mustache 标签,也就是如果需要显示两个大括号就可以用{ {}}
- 跳过不需要编译的节点,加快编译速度
<template id="my-app">
<h2>{
{msg}}</h2>
<h2 v-pre>{
{msg}}</h2>
</template>
(了解)v-cloak 指令
这个指令保持在元素上直到关联组件实例结束编译
- 和 css 规则如 [v-cloak]{display:none} 一起用时,这个指令可以隐藏未编译的 Mustache 标签到组件实例准备完毕
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<!-- 在 vue 编译的时候,有可能会出现一个情况就是 {
{}} 中会显示一下 msg 这个变量名,然后编译完成后在替换成对应的值,情况很少,但是也可能会出现 -->
<!-- 为了防止这个情况出现,我们可以给标签加上 v-cloak属性,配合 display:none 可以让在没有编译完成之前先隐藏,并且解析完成就会自动删掉这个属性,编译完成后在显示 -->
<h2 v-cloak>{
{msg}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
setTimeout(()=>{
const App = {
template: '#my-app',
data() {
return {
msg: 'hello world'
}
},
}
Vue.createApp(App).mount('#app')
},3000)
</script>
</body>
</html>
条件渲染指令
- Vue 中为我们提供了四种渲染指令
- v-if
- v-else
- v-else-if
- v-show
v-if 单独使用
<h2 v-if="flag">{
{msg}}</h2>
<script>
const App = {
template: '#my-app',
data() {
return {
msg: 'hello world',
flag: true
}
}
}
Vue.createApp(App).mount('#app')
</script>
v-if 与 v-else
<h2 v-if="flag">{
{msg}}</h2>
<h2 v-else>{
{msg1}}</h2>
<script>
const App = {
template: '#my-app',
data() {
return {
msg: 'hello world',
msg1: 'hi~',
flag: true
}
}
}
Vue.createApp(App).mount('#app')
</script>
v-if 与 v-else-if 与 v-else
<h2 v-if="count < 10">{
{msg}}</h2>
<h2 v-else-if="count > 10 && count < 20">{
{msg1}}</h2>
<h2 v-else>{
{msg2}}</h2>
<script>
const App = {
template: '#my-app',
data() {
return {
msg: 'hello world',
msg1: 'hi~',
msg2: 'hello~',
count: 21,
}
}
}
Vue.createApp(App).mount('#app')
</script>
v-if 的渲染原理
- v-if 是惰性的
- 当条件为 false 时,其判断的内容完全不会被渲染或者会被销毁掉
- 当条件为 true 时,才会真正渲染条件块中的内容
template 元素
- 因为 v-if 是一个指令,必须依托于元素上
- 所以如果我们希望切换多个元素,可以使用 template 标签包裹,可以省去渲染 div
v-show
- v-show 和 v-if 使用方法差不多是一致的,也是通过一个条件决定是否显示元素或组件,因此关于 v-show 不在进行代码演示
v-show 和 v-if 的区别
-
v-show 是通过样式 display: none 来达到隐藏效果的,而 v-if 是通过销毁和创建元素来实现隐藏和显示的
◼ 首先,在用法上的区别:
v-show是不支持template;
v-show不可以和v-else一起使用;
◼ 其次,本质的区别:
v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有存在的,只是通过CSS的display属性来进行切换;
v-if当条件为false时,其对应的原生压根不会被渲染到DOM中;
◼ 开发中如何进行选择呢?
如果我们的原生需要在显示和隐藏之间频繁的切换,那么使用v-show;
如果不会频繁的发生切换,那么使用v-if;
-
我们来看一下 v-show 的结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wijt2JVi-1689141095303)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230610195258289.png)]
-
v-if 的结构图如下:
列表渲染
v-for
- 我们可以使用 v-for 来帮助我们完成一些结构重复但是数据不重复的页面展示
v-for 的基本使用
-
语法为:item in arrary/object
<template id="my-app"> <h2 v-for="l in list">{ { l.name }}</h2> </template> <script> const App = { template: '#my-app', data() { return { list: [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }, { id: 4, name: '赵六' }, { id: 5, name: '田七' } ] } } } Vue.createApp(App).mount('#app') </script>
v-for 渲染时的获取索引
-
有时候我们需要获取此次渲染列表的索引,我们可增加第二个参数 index,即可获取索引
<template id="my-app"> <h2 v-for="(l,index) in list">{ { index }}{ { l.name }}</h2> </template> <script> const App = { template: '#my-app', data() { return { list: [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }, { id: 4, name: '赵六' }, { id: 5, name: '田七' } ] } } } Vue.createApp(App).mount('#app') </script>
v-for 遍历对象
<template id="my-app">
<ul>
<!-- 只写一个参数获取的是 值 -->
<li v-for="value in info">{
{ value }}</li>
<!-- 第一个参数是 value,第二个参数就是 key,第三个参数就是索引 -->
<li v-for="(value, key, index) in info">{
{key}} + {
{value}} + {
{index}}</li>
</ul>
</template>
<script>
const App = {
template: '#my-app',
data() {
return {
info: {
name: '张三',
age: 18,
address: '长沙'
}
}
}
}
Vue.createApp(App).mount('#app')
</script>
v-for 遍历数字
<template id="my-app">
<ul>
<!-- 当然也是具备索引的 -->
<li v-for="item in 10">{
{item}}</li>
</ul>
</template>
<script>
const App = {
template: '#my-app',
data() {
return {
}
}
}
Vue.createApp(App).mount('#app')
</script>
搭配 template 使用
<template v-for="l in list">
<div>{
{ l.name }}</div>
</template>
<script>
const App = {
template: '#my-app',
data() {
return {
list: [
{
id: 1, name: '张三' },
{
id: 2, name: '李四' },
{
id: 3, name: '王五' },
{
id: 4, name: '赵六' },
{
id: 5, name: '田七' }
]
}
}
}
Vue.createApp(App).mount('#app')
</script>
数组更新检测
-
Vue 将被侦听的数据的变更方法进行了包裹,所以它们也将触发视图更新,这些方法包括:push() pop() shift() unshift() splice() sort() reverse()
<template id="my-app"> <ul> <li v-for="l in list">{ { l.name }}</li> </ul> <hr> <button @click="increase">添加成员</button> <button @click="decrease">删除成员</button> </template> <script> const App = { template: '#my-app', data() { return { list: [ { name: '张三' }, { name: '李四' }, { name: '王五' }, { name: '赵六' }, { name: '田七' } ], } }, methods: { increase() { this.list.push({ name: '丁八' }) }, decrease() { this.list.pop() } }, } Vue.createApp(App).mount('#app') </script>
v-for 中 key 的作用
- 在使用 v-for 时,我们通常都会给元素绑定一个 key 属性
- 为什么需要 key,官方的解释如下:
- key 属性主要用在 Vue 的
虚拟 DOM 算法
,在新旧 nodes
对比时编写VNodes
- 如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地
修改/复用相同类型元素
的算法 - 而使用 key,它会基本 key 的变化
重新排列元素顺序
,并且移除/销毁key
不存在的元素
- key 属性主要用在 Vue 的
认识 VNode
-
VNode的全称是 Virtual Node,也就是虚拟节点,因为还没有聊到组件,这里暂时可以把 VNode 看成是 HTML 元素创建出来的 VNode
-
页面中的展示的div等元素,就是真实节点,虚拟的节点就是存在与内存中的JavaScript对象
-
即页面当中所展示的元素,在 Vue 中就是以 VNode 的形式存在
-
VNode 的本质是一个对象,看一下示例的 div 结构,如下:
<div class="box", style="color: #f40"> hello </div>
-
在 Vue 中转为一个虚拟节点如下:
const vnode = { type:"div", props:{ class: "box", style:{ color: '#f40' } }, children: 'hello' }
-
在 Vue 中会先把模板转为一个个的 VNode ,在转为真实的 dom,多了异步中间的虚拟节点,最大的好处就是多平台适配
虚拟 DOM
虚拟DOM一个重要的作用是跨平台,实现真实DOM,渲染在浏览器;实现移动端的button/view/image,实现桌面端的一些控件,甚至实现VR设备。还有一个作用是可以进行diff算法
-
如果我们不只是一个简单的 div,而是有一大堆的元素,那么他们应该会形成一个 VNode Tree
-
下面我们来举例一个场景,假设我们初次渲染了四个元素,a b c d,然后生成的真实的 dom a b c d
-
那么我们如果在中间添加一个元素 e,怎么样渲染效率最高呢?肯定其他四个元素不变,在添加一个新元素,如果需要实现这个效果,就涉及到了一个 diff 算法
diff 算法
- diff 算法是怎么计算的,现在我们
旧的 VNodes
是 a b c d,新的VNodes
是 a b e c d,那么现在能不能通过一个什么手段,让新旧的 VNodes 来进行一个对比,然后经过对比只要获取需要新增的 e 元素进行添加即可,这个新旧 VNodes 对比的过程就叫做 diff 算法
没有 key 的情况下渲染过程
-
在没有 key 的情况下, Vue 会执行
patchUnkeyedChildren
方法 -
还是回到原来的例子,旧VNodes 为 a b c d,新VNodes 为 a b e c d,这个方法就会取两个新旧数组的长度,以长度最小的为标准开始遍历,我们这里旧的比较小,长度为 4,我们可以用伪代码大概表示一下:
const newArr = ['a', 'b', 'e', 'c', 'd'] const oldArr = ['a', 'b', 'c', 'd'] for(let i = 0; i < oldArr.length; i++){ // 根据索引获取两数组的同一元素对比 if(newArr[i] === oldArr[i]){ // 相等就执行 patch 方法,更新 } else { // 不相等也是 patch 但是会执行内部的判断,比如判断元素什么是否相等 // e 就会更新 旧数组 的 c 元素上,e 到 d } } // 最后遍历完成后,因为 新的 比 旧的 多。就会创建两个差值的元素在添加上去 // 比如这里 5 - 4 = 1,就会创建一个元素,并直接把 新的VNode 中从长度 4 之后的元素 更新上去 // 如果 新的 比 旧的 短,那么对比完成后,就直接把后面的全部删掉
-
从上面可以看出来,并不是我们最开始那种设想的理想渲染,因此效率较低,因此我们不难发现没有 key 的情况下我就尽可能的复用,如果后面的不能复用了,类型相等,就把类型复用,内容修改,少则增之多则删之,我们就可以看回最开始的概念,如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地
修改/复用相同类型元素
的算法,如图:
有 key 的情况下渲染过程
-
如果有 key,就会执行
patchKeyedChildren
方法 -
我们这里依然以
新节点为 a b e c d
,旧节点为 a b c d
-
情况一:新节点长度 大于 旧节点,它是先从数组首部比较,a b 都是相等的,遇到新节点的 e 的时候发现 与旧节点的 c 不相等,那么就会从尾部开始比较, 新d 与 旧d 比较相等,新c 与 旧c 相等,然后发现现在中间剩余中间的部分,也就是 e,简单总结一下就是 和 b 新旧中是一样的就会继续比较,但是新旧中的 e 和 c 不一致就会跳出本次循环,就如从尾部开始比较,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f41xLYgp-1689141095304)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230610215620651.png)]
-
第三步就是我们发现了旧节点遍历完毕,在新节点中还有一个 e,就直接新增节点,如图:
-
第四步就是另外一种情况,比如旧节点比新节点多的时候,遍历完首尾之后就删掉原来的旧节点,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8jCUBM5-1689141095304)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230610215647696.png)]
-
第五步就是最特殊的一种情况,我们例子中是不多不少的,实际情况可能多也可能少的,中间这部分是
无序的
,中间这部分怎么比较呢?它会创建一个数组,然后根据 key 找到对应的索引放入对应的位置,即尽量在旧节点中找到相同的,然后复用,如果新节点中有,旧节点中没有的,就新增一个,如果新节点中没有,旧节点中有,就删除一个,如图:
计算属性-options-API
复杂的 data 处理方式
- 在模板 template 中,我们会使用 插值语法 显示一些 data 中的数据
- 但是有时候的数据直接写在 { {}} 中,难免会显得很臃肿,也不美观,也难以维护
- 比如最常见的
三元运算符
、多个 data 数据的计算
、某种数据的格式化
,这些都会造成代码的臃肿甚至是不利于后续的维护 - 并且最麻烦的地方在于如果多个地方使用到同一段代码逻辑,会让我们编写许多重复性的代码
- 我们可使用
methods
变成一个方法来使用,当然更好的是使用计算属性computed
什么是计算属性
- 对于此官方并没有明确的解释
- 而是建议我们:对于任何包含响应式数据的复杂逻辑,你都应该使用
计算属性
计算属性
将被混入到组件实例中,所有的 getter 和 setter 的 this 上下文自动绑定为组件实例
计算属性的基本使用
- 我们通过三个案例来具体了解一下计算属性的基本使用
案例一
我们有两个变量:name1 和 name2 ,希望他们拼接之后在界面上显示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<h2>{
{name}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
name1: '张',
name2: '三',
}
},
computed: {
name() {
// 计算属性依靠返回值决定结果
return this.name1 + this.name2
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
案例二
我们有一个分数 scope,当 scope 大于 60 的时候显示
及格
,小于 60 的时候显示不及格
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<h2>{
{result}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
scope: 66,
}
},
computed: {
result() {
return this.scope > 60 ? '及格' : '不及格'
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
案例三
当我们有一个变量 msg,记录一段文字 Hello World,某些情况下直接显示文字,某些情况下需要将这段文字翻转
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<h2>{
{reversal}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
msg: 'Hello World'
}
},
computed: {
reversal() {
return this.msg.split(' ').reverse().join(" ")
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
计算属性-缓存
-
计算属性最大的好处在于,
缓存
,也就是说多次使用一个计算属性的时候,只要这个计算属性所依赖的值没有改变,就只会执行一次 -
结果如图:
计算属性的 getter 和 setter
getter
-
计算属性大多数情况下,只需要一个 getter 方法,所以我们也通常会将计算属性写成一个函数,如下:
computed: { name() { return this.name1 + this.name2 } },
setter
-
如果需要 getter 方法也需要 setter方法,我们需要书写计算属性的完整写法,如下:
computed: { name() { return this.name1 + this.name2 }, set(newVal) { console.log('计算属性name正在被修改:', newVal) } },
-
当我们直接修改计算属性看一下,会不会触发 set 方法,如图:
-
因此我们可以在这里触发 set 方法时,修该 name1 和 name2 属性,当这两个属性被修改时,会自动触发 get 方法,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <template id="my-app"> <h2>{ {name}}</h2> <button @click="updateName">修改name</button> </template> <script src="../js/vue.js"></script> <script> const App = { template: '#my-app', data() { return { name1: '张', name2: '三' } }, computed: { name: { get() { return this.name1 + this.name2 }, set(newVal) { console.log('计算属性name正在被修改:', newVal) const res = newVal.split(' ') this.name1 = res[0] this.name2 = res[1] } } }, methods: { updateName() { // 直接修改计算属性 name this.name = '李 四' } } } Vue.createApp(App).mount('#app') </script> </body> </html>
watch 侦听器
什么是侦听器
- 在 data 返回的对象中定义了数据,这个数据通过
插值语法等方式绑定到 template 中
- 当数据变化时。template 会自动进行更新来显示最近的数据
- 但是在某些情况下,我们希望在
代码逻辑
中监听某个数据的变化,这个时候就需要用侦听器watch
来完成
watch 的基本使用
-
watch 语法:
watch:{ // 可以侦听 data 中的属性也可以侦听 props 中的属性 // - newVal 新值 // - oldVal 旧值 ['属性名'](newVal, oldVal){ console.log(newVal) } }
-
利用 watch 侦听某一个数据,来进行一些逻辑处理,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <template id="my-app"> 问题:<input type="text" v-model="msg"> 回复: <textarea v-model="answer" name="" id="" cols="30" rows="10"></textarea> </template> <script src="../js/vue.js"></script> <script> const App = { template: '#my-app', data() { return { msg: 'hello world', answer: '', info:{ name:'aaa', age:18 } } }, methods: { queryAnswer(val) { this.answer = `你的问题是:${ val}\n 答案是:大河之剑天上来` } }, watch: { msg(newVal) { this.queryAnswer(newVal) }, info(newVal){ console.log(Vue.toRaw(newVal))//拿到原始对象 } } } Vue.createApp(App).mount('#app') </script> </body> </html>
配置选项
深度监听
-
我们已经测试过了,可以监听data中的一级属性,那么假设我们现在 data 中的 info 数据如下:
data() { return { msg: 'hello world', info: { name: '张三', age: 18 } } },
-
修改数据方法如下:
methods: { update() { console.log('修改方法触发~') this.info.name = '李四' } },
-
注意,
我们修改的是 info 的 name 属性
,不是info 本身,监听器如下:watch: { info(newVal, oldVal) { console.log(newVal, oldVal) } }
-
看一下输入结果,监听器能否监听,如图:
-
方法触发,但是监听器没有触发,说明没有监听到,因此我们需要进行深度监听,而如果需要开启深度监听的话,就需要改变 watch 的写法,如下:
watch: { // 深度监听 info: { // 新旧值变化通过 handler 函数监听 handler(newVal, oldVal) { console.log(newVal, oldVal) }, // deep 属性设置为 true 表示开启深度监听 deep: true } }
-
输出结果如图:
-
虽然深度监听也可以监听数组,但是一般不会直接监听这个数组,在实际开发中怎么监听呢?假设现在数组的值为 users = [{ name: ‘zs’ },{ name: ‘ls’ }],那么我们会创建一个子组件,比如子组件叫
user-name
,那我们会在父组件中使用v-for 来遍历子组件
,并且给子组件传递一个属性,就是数组的每一项,子组件通过 props:[‘n’] 接收传入的对象,我们只需要在子组件中监听 props 中的 n 这个对象即可
当然,这里看到新旧值都是李四,是因为开启深度监听之后,新旧值就会相同,因为它们的引用指向同一个对象/数组,Vue 不会保留之前值的副本,没有副本也就是说不会对原来的数组做一个深拷贝来保存,而是直接的引用赋值,所以改动了一个之后,新旧值因为都指向一个地址,所以显示的数据都是修改之后的,因此显示就会相同
立即执行
-
当我们首次进入页面或者刷新页面的时候,并不会执行监听,因为没有值发生变化,如果我们有需求需要进行立即执行的话,可以配置
立即执行
选项 -
如下:
watch: { info: { handler(newVal, oldVal) { console.log(newVal, oldVal) }, // 立即执行 immediate: true } }
侦听器的其他写法一
-
传入一个回调数组,里面的函数会依次执行,如下:
methods:{ foo() { console.log(this.testArr, '我是 foo') } } watch:{ // 传入一个回调数组 testArr: [ // 定义在 methods 中的方法 'foo', // 直接书写函数 function handler1() { console.log('监听testArr, 执行函数一') }, // 可以接收新旧值 function handler2(newVal, oldVal) { console.log('监听testArr, 执行函数二', '新值:' + newVal, '旧值:' + oldVal) }, { // 也可以使用对象包裹,并将键值命名为 handler 即可同样可以接收新旧值 handler: function (newVal, oldVal) { console.log('我是对象包裹的函数', newVal, oldVal) } } ] }
-
我们可以修改一下 testArr 的值,来看一下执行顺序,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n1Z8NqHO-1689141095306)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230612103247112.png)]
侦听器的其他写法二
-
在 Vue3 的文档中没有提到,在 Vue2 的文档中有提到,监听对象属性的写法,如下:
'info.name': { handler(newVal, oldVal) { console.log(newVal, oldVal) }, // 立即执行 immediate: true },
-
我们可以看一下结果,如图:
-
当然也可能是数组中的某一项的属性,如下:
'arr[0].name': { handler(newVal, oldVal) { console.log(newVal, oldVal) }, // 立即执行 immediate: true },
使用 $watch 的 API
-
我们可与在 created 或其他生命周期中,使用 this.$watch 来侦听
- 第一个参数的
侦听的源
- 第二个参数是
侦听的回调函数
- 第三个参数是
额外的其他选项
,比如 deep、immediate
- 第一个参数的
-
代码如下:
created() { this.$watch('info', (newValue, oldValue) => { console.log('执行 this.$watch~') console.log(newValue, oldValue) }, { deep: true, immediate: true }) },
-
结果如图:
-
这个方法还具备一个返回值,返回一个函数,当你调用这个函数时,就可以取消监听,如下:
created() { const res = this.$watch('info', (newValue, oldValue) => { console.log('执行 this.$watch~') console.log(newValue, oldValue) }, { deep: true, immediate: true }) // 调用函数取消监听 res() },
v-model
v-model 的基本使用
-
在开发中
表单提交
的功能时非常常见的,如:- 比如登录注册时需要提交账号密码
- 在检索、创建、更新信息时,需要提交一些数据
-
这些功能需要我们可以获取到用户提交的数据,在 Vue 中,v-model 可以很好的帮我们实现这一点
- v-model 可以在表单 input、textarea 以及 select 元素上创建
双向数据绑定
- 它会根据
控件类型
自动选择正确的方法来更新元素 - v-model 本质上也只是语法糖,
负责监听用户的输入事件来更新数据
,并在某种极端场景下进行特殊处理
- v-model 可以在表单 input、textarea 以及 select 元素上创建
-
使用 v-model 也非常简单,在 html 标签中,如下:
<template id="my-app"> 姓名:<input type="text" v-model="username" /> </template>
-
data 中定义变量数据即可,如下:
data() { return { username: '请输入名称' } },
-
这样即可实现变量值会被当做 input 的初始值,input 更新时会同步更新 data 中变量的值,
v-model 的实现原理
-
本质上 v-model 其实也就是一个
语法糖
,:value="username"实现了data里的值绑定到输入框里 -
是 input 使用 v-bind 动态绑定了
value
属性, -
并使用 v-on 监听了 input 事件监听,如下:
<input type="text" :value="username" @input="onChange"/> <script> data() { return { username: "Hello Model", account: "", password: "" } }, methods:{ onChange(event){ this.username = event.target.value } } </script>
v-model 绑定其他表单元素
textarea
-
html 代码如下:
<template id="my-app"> <label for="desc"> 描述 <textarea name="" id="desc" cols="30" rows="10" v-model="msg"></textarea> </label> <h2>desc:{ {msg}}</h2> </template>
-
data 代码如下:
data() { return { msg: '请输入名称' } },
checkbox
单选框:单选框不需要绑定value
-
html 代码如下:
<label for="agree"> <input type="checkbox" v-model="isChenkBox" id="agree">同意协议 </label> <p>是否选中:{ {isChenkBox}}</p>
-
data 代码如下:
data() { return { isChenkBox: false } },
多选框
-
html 代码如下:多选框必须要具备一个 value 属性
<span>爱好:</span> <!-- 如果是多选框的话,绑定一个变量为数组,并且必须要具备一个 value 属性 --> <label for="basketball"> <input type="checkbox" v-model="hobbies" value="basketball" id="basketball">篮球 </label> <label for="pingpong"> <input type="checkbox" v-model="hobbies" value="pingpong" id="pingpong">乒乓球 </label> <label for="football"> <input type="checkbox" v-model="hobbies" value="football" id="football">足球 </label> <p>hobbies:{ {hobbies}}</p>
-
data 代码如下:
data() { return { hobbies: [] } },
radio
-
html 代码如下:radio必须要具备一个 value 属性,不用写name="gender"来让radio互斥,v-model本身就会有互斥的效果,因为有变量控制变化
<span>性别</span> <label for="man"> <input type="radio" v-model="gender" value="man" id="man"> 男 </label> <label for="girl"> <input type="radio" v-model="gender" value="girl" id="girl"> 女 </label> <p>gender:{ {gender}}</p>
-
data 代码如下:
data() { return { gender: "" } },
select
-
代码如下:
<div id="app"> <!-- select的单选 --> <select v-model="fruit"> <option value="apple">苹果</option> <option value="orange">橘子</option> <option value="banana">香蕉</option> </select> <h2>单选: { {fruit}}</h2> <hr> <!-- select的多选 --> <select multiple size="3" v-model="fruits"> <option value="apple">苹果</option> <option value="orange">橘子</option> <option value="banana">香蕉</option> </select> <h2>多选: { {fruits}}</h2> </div> <script src="../lib/vue.js"></script> <script> // 1.创建app const app = Vue.createApp({ // data: option api data() { return { fruit: "orange", fruits: [] } }, }) // 2.挂载app app.mount("#app") </script>
v-model值绑定
◼ 目前我们在前面的案例中大部分的值都是在template中固定好的:
比如gender的两个输入框值male、female;
比如hobbies的三个输入框值basketball、football、tennis;
◼ 在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到data返回的对象中,再通过v-bind来进行值的绑定,这个过程就是**值绑定**。
这里不再给出具体的做法,因为还是v-bind的使用过程。
<div id="app">
<!-- 1.select的值绑定 -->
<select multiple size="3" v-model="fruits">
<option v-for="item in allFruits"
:key="item.value"
:value="item.value">
{
{item.text}}
</option>
</select>
<h2>多选: {
{fruits}}</h2>
<hr>
<!-- 2.checkbox的值绑定 -->
<div class="hobbies">
<h2>请选择你的爱好:</h2>
<template v-for="item in allHobbies" :key="item.value">
<label :for="item.value">
<input :id="item.value" type="checkbox" v-model="hobbies" :value="item.value"> {
{item.text}}
</label>
</template>
<h2>爱好: {
{hobbies}}</h2>
</div>
</div>
<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {
// 水果
allFruits: [
{
value: "apple", text: "苹果" },
{
value: "orange", text: "橘子" },
{
value: "banana", text: "香蕉" },
],
fruits: [],
// 爱好
allHobbies: [
{
value: "sing", text: "唱" },
{
value: "jump", text: "跳" },
{
value: "rap", text: "rap" },
{
value: "basketball", text: "篮球" }
],
hobbies: []
}
}
})
// 2.挂载app
app.mount("#app")
</script>
v-model 的修饰符
lazy
-
有些时候我们并不需要每一次改动都需要变化,而是当我输入完成失去焦点或者按下回车时才改变,这时候我们就可以使用
修饰符 lazy
-
代码如下:
<input type="text" v-model.lazy="msg">
-
修饰符的作用就是将原来 input 标签的 @input 事件改为 @change 事件,从而改变触发时间
number
-
在默认情况下,我们在 data 中设置 num 的值 10,是一个数字型,但是经过 v-model 绑定之后,只要经过赋值,也会变成字符串,如果我们希望变为数字型,只需要使用
修饰符 number
即可 -
代码如下:
<input type="text" v-model.number="num">
trim
-
去除用户输入时前后输入的空格,如果监听trimMsg,监听不到
-
代码如下:
<input type="text" v-model.trim="trimMsg">
Vue 组件化开发
注册全局组件
全局组件一旦注册成功,可以在任何template中使用
定义组件名的方式有两种:
◼ 方式一:使用kebab-case(短横线分割符)
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my
component-name>;html中不能识别大写,在html眼中只有小写,但是在template中大小写都可以
◼ 方式二:使用PascalCase(驼峰标识符)
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。
也就是说 和 都是可接受的;
-
Vue.createApp(App) 传入一个 APP 配置对象的时候,是有一个返回值的,因此 Vue.createApp(App).mount(‘#app’) 可以被拆分成如下代码:
const app = Vue.createApp(App) app.mount('#app')
-
因此我们也可以在 app 这个对象身上
注册一个组件
,使用方法compoment
-
component 方法具备两个参数,
- 参数一:组件名称
- 参数二:组件配置对象
-
js 代码如下:
<script> const App = { template: '#my-app', data() { return { } }, methods: { }, } const app = Vue.createApp(App) // 使用app注册一个组件,并且这个组件是一个全局组件 // - 使用 component(组件名称,组件对象) 方法 app.component('component-a', { template: `<h2>我是 component-a 组件</h2>` }) app.mount('#app') </script>
-
html 代码如下:
<template id="my-app"> <h2>我是标题</h2> <p>我是内容, 大河之间天上来</p> <!-- 直接在 template 使用即可 --> <component-a></component-a> </template>
-
可以看一下是否能正常显示,如图:
-
当然也可以把 template 模板的这个结构抽离出去,同样通过 id 绑定即可,首先我们将 component-a 组件中的 html 结构提取出来 如下:
<template id="component-a"> <h2>我是 component-a 组件</h2> <h2>我是 component-a 组件</h2> </template>
-
然后在这个 component-a 组件的 template 属性更换成 component-a 这个 id,如下:
app.component('component-a', { template: `#component-a` })
-
然后在我们原先的模板中直接使用 即可,如下:
<template id="my-app"> <h2>我是标题</h2> <p>我是内容, 大河之间天上来</p> <!-- 直接在 template 使用即可 --> <component-a></component-a> <component-a></component-a> </template>
-
效果如图:
-
全局组件意味着
这个组件可以在任意的组件模板中使用
-
同样,这个 component-a 组件也可以有 data、watch、methods等配置,代码如下:
app.component('component-a', { template: `#component-a`, data(){ return { title:'我是 component-a 组件的标题' } } })
-
将其应用在 component-a 组件的 html 结构里面,如下:
<template id="component-a"> <h1 style="color:#f40;">{ { title }}</h1> <h2>我是 component-a 组件</h2> </template>
-
结果如图:
注册局部组件
-
为什么需要使用局部组件,因为如果时全局组件注册的话,如果这个
全局组件没有被用到
,但是这个全局注册的组件依然被我们注册了,就意味着使用类似 webpack 这种打包工具在打包我们的项目时,我们依然会对其进行打包
,从而导致我们打包的项目体积增大 -
我们应该如何注册这个局部组件呢,首先我们先定义好这个局部组件的 html 结构,如下:
<template id="component-a"> <h2 style="color: #f40;">我是 component-a 局部组件</h2> </template>
-
然后定义好局部组件的
配置对象
,如下:// 定义局部组件配置对象 const ComponentA = { template: '#component-a' }
-
关键的来了,现在我们不需要使用 app.component() 这个方法,而是在
需要使用这个局部组件的配置对象
里面加一个属性components
,如下:const App = { template: '#my-app', data() { return { msg: 'hello world' } }, // 添加 components 属性 components: { // 采用键值对的方式,键决定在当前组件内应该以什么名称使用,值就是这个局部组件本身 // - 比如我们将 ComponentA 改为 c-a 'c-a': ComponentA } }
-
然后在当前组件直接使用即可,如下:
<template id="my-app"> <h2>{ {msg}}</h2> <c-a></c-a> <c-a></c-a> </template>
-
结果如图:
-
我们看一下完整的代码,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <template id="my-app"> <h2>{ {msg}}</h2> <c-a></c-a> <c-a></c-a> </template> <template id="component-a"> <h2 style="color: #f40;">我是 component-a 局部组件</h2> </template> <script src="../js/vue.js"></script> <script> // 定义局部组件配置对象 const ComponentA = { template: '#component-a' } const App = { template: '#my-app', data() { return { msg: 'hello world' } }, // 添加 components 属性 components: { // 采用键值对的方式,键决定在当前组件内应该以什么名称使用,值就是这个局部组件本身 // - 比如我们将 ComponentA 改为 c-a 'c-a': ComponentA } } const app = Vue.createApp(App) app.mount('#app') </script> </body> </html>
- 案例:
<div id="app">
<home-nav></home-nav>
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>
<template id="product">
<div class="product">
<h2>{
{
title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {
{
price}}</p>
<button>收藏</button>
</div>
</template>
<template id="nav">
<div>-------------------- nav start ---------------</div>
<h1>我是home-nav的组件</h1>
<product-item></product-item>
<div>-------------------- nav end ---------------</div>
</template>
<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const ProductItem = {
template: "#product",
data() {
return {
title: "我是product的title",
price: 9.9
}
}
}
// 1.1.组件打算在哪里被使用
const app = Vue.createApp({
// components: option api
components: {
ProductItem,
HomeNav: {
template: "#nav",
components: {
ProductItem
}
}
},
// data: option api
data() {
return {
message: "Hello Vue"
}
}
})
// 2.挂载app
app.mount("#app")
</script>
.browserslistrc和jsconfig.json讲解
jsconfig.json结论:可以直接删除,因为是配置vscode的,可以给我们更好的代码提示,不关代码事
main.js按照以前的方式书写
(不推荐,也不方便)
法一:
import {
createApp} from 'vue/dist/vue.esm-bundler'//由vue源码中的compile来解析template转换为createVNode
createApp({
template:`<h2>我是标题</h2>`
}).mount('#app')
法二:这样又回到了之前的写法,不推荐
import {
createApp} from 'vue/dist/vue.esm-bundler'
const App = {
template: `<h2>{
{title}}</h2>`,
data() {
return {
title: "我也是标题"
}
}
}
createApp(App).mount('#app')
采用.vue文件书写方式优化:
- main.js
import {
createApp } from 'vue/dist/vue.esm-bundler'
import App from './components/App.vue'
const app = createApp(App)
app.mount('#app')
- App.vue export defaultde的由来就是导出去供main.js的使用
<template>
<h2>{
{title}}</h2>
<h2>当前计数: {
{counter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</template>
<script>
export default {
data() {
return {
title: "我还是标题",
counter: 0
}
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
}
</script>
<style>
h2 {
color: red;
}
</style>
配置地址别名
vue.config.js
const {
defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
// 配置路径别名
// @是已经配置好的路径别名: 对应的是src路径
alias: {
"utils": "@/utils"
}
}
}
})
jsconfig.js
让vscode有地址的提示
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
],
"utils/*": [
"src/utils/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
解释vue中两种引入方式
import {
createApp } from 'vue' // 不支持template选项
// import { createApp } from 'vue/dist/vue.esm-bundler' // compile代码
import App from './App.vue' // vue-loader: template -> createVNode过程
import "./utils/abc/cba/nba/index"
/**
* 1.jsconfig.json的演练
* 作用: 给VSCode来进行读取, VSCode在读取到其中的内容时, 给我们的代码更加友好的提示.
* 2.引入的vue的版本
* 默认vue版本: runtime, vue-loader完成template的编译过程
* vue.esm-bundler: runtime + compile, 对template进行编译
*
* 3.补充: 单文件Vue style是有自己的作用域
* style -> scoped
* 4.补充: vite创建一个Vue项目
*/
// 元素 -> createVNode: vue中的源码来完成
// compile的代码
// const App = {
// template: `<h2>Hello Vue3 App</h2>`,
// data() {
// return {}
// }
// }
createApp(App).mount('#app')
组件的通信
组件之间的关系
-
先来看一下组件的嵌套逻辑关系图,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JbvarI3A-1689141095309)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230613224156556.png)]
-
App 组件是 Header Main Footer 组件的
父组件
-
Main 组件是 Banner ProductList 组件的
父组件
父子组件之间的通信方式
父传子
通过 props 属性
什么是 props
- props 是可以在组件上
注册一些自定义的 attribute(属性)
- 父组件给这些
attribute(属性)
赋值,子组件通过attribute(属性)
的名称获取到对应的值
props 的常用语法
字符串数组:
数组中的字符串就是 attribute 的名称对象类型:
对象类型可以指定 attribute 名称的同时,还可以指定传递的数据类型,是否必须传递,默认值等
props-非动态绑定属性-数组形式
-
父组件:APP.vue 文件
<template> <h2>App</h2> // 非动态绑定属性-传入的值默认是字符串 <show-messageVue test="test" msg="大河之剑天上来" title="李白"></show-messageVue> </template> <script> import ShowMessageVue from './ShowMessage.vue' export default { components: { ShowMessageVue }, data() { return { test: '测试文本', } } } </script> <style scoped> h2 { color: #f40; } </style>
-
子组件:ShowMessage.vue文件
<template> <h2>{ { title }}</h2> <p>{ { msg }}</p> <p>{ { test }}</p> </template> <script> export default { // 数组写法 props: ['msg', 'title', 'test'] } </script> <style scoped></style>
-
展示效果如图:
props-动态绑定属性-数组形式
-
父组件:APP.vue 文件
<template> <h2>App</h2> // 动态绑定属性-以属性值为准 <show-messageVue msg="大河之剑天上来" title="李白"></show-messageVue> </template> <script> import ShowMessageVue from './ShowMessage.vue' export default { components: { ShowMessageVue }, data() { return { test: '测试文本', msg: '大河之剑天上来', title: '李白' } } } </script> <style scoped> h2 { color: #f40; } </style>
-
子组件:ShowMessage.vue文件
<template> <h2>{ { title }}</h2> <p>{ { msg }}</p> <p>{ { test }}</p> </template> <script> export default { // 数组写法 props: ['msg', 'title', 'test'] } </script> <style scoped></style>
-
展示效果如图:
props-对象写法
-
语法如下:
props: { msg: String, // 规定为 string 类型 title: { type: String, // 类型 required: true, // 必传的 default: '111' // 默认值 } }
-
也可以设置多个类型,如下:
props: { msg: String, // 规定为 string 类型 test:[String, Number], title: { type: String, // 类型 required: true, // 必传的 default: '111' // 默认值 } }
-
对象或数组的默认值必须是一个函数,如下:
props: { msg:{ type:Object, default(){ return { name: 'zs' } } } }
-
自定义验证函数,当希望传入的值是我们规定的几个值以内,如下:
props: { msg:{ validator(value){ // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].includes(value) } } }
非 props 属性
-
一个组件中如 class 这种属性父组件传过来了,但是子组件内部中的 props 属性没有定义,且子组件有一个根节点,
就会把属性添加到组件的根节点上
,父组件中如下:<template> <h2>App</h2> <!-- 定义非props属性 class --> <show-messageVue class="xxx" :test="test" :msg="msg" :title="title"></show-messageVue> </template> <script> import ShowMessageVue from './ShowMessage.vue' export default { components: { ShowMessageVue }, data() { return { test: '测试文本', msg: '大河之剑天上来', title: '李白' } } } </script> <style scoped> h2 { color: #f40; } </style>
-
子组件如下:
<template> <div> <h2>{ { title }}</h2> <p>{ { msg }}</p> <p>{ { test }}</p> </div> </template> <script> export default { // props: ['msg', 'title', 'test'] props: { msg: String, // 规定为 string 类型 title: { type: String, // 类型 required: true, // 必传的 default: '111' // 默认值 } } } </script> <style scoped></style>
-
查看dom结构,如图:
-
如果需要
禁止这种继承
,只需要在组件中添加一个属性,inheritAttrs: false,
,如下:<template> <div> <h2>{ { title }}</h2> <p>{ { msg }}</p> <p>{ { test }}</p> </div> </template> <script> export default { // 禁止继承属性 inheritAttrs: false, // props: ['msg', 'title', 'test'] props: { msg: String, // 规定为 string 类型 title: { type: String, // 类型 required: true, // 必传的 default: '111' // 默认值 } } } </script> <style scoped></style>
-
dom结构如图:
-
当我们希望给的一个
非props属性
不给到根元素的时候,可以禁用,比如我们这里希望给到 h2 元素,而不是 div 根元素,就可以禁止 -
我们可以通过
$attrs
来访问所有的非props属性
,只需要给 h2 标签上使用 $attrs.class 既可以获取外部给的非props属性 class,如下:<template> <div> <h2 :class="$attrs.class">{ { title }}</h2> <p>{ { msg }}</p> <p>{ { test }}</p> </div> </template> <script> export default { // 禁止继承属性 inheritAttrs: false, // props: ['msg', 'title', 'test'] props: { msg: String, // 规定为 string 类型 title: { type: String, // 类型 required: true, // 必传的 default: '111' // 默认值 } } } </script> <style scoped></style>
-
dom结构如图:
-
当我们存在多个根节点,也没有禁止继承的话,是什么情况,父组件如下:
<template> <h2>App</h2> <show-messageVue class="xxx" :test="test" :msg="msg" :title="title"></show-messageVue> <!-- 设置id属性 --> <hello-world id="aaa"></hello-world> </template> <script> import HelloWorld from './HelloWorld.vue' import ShowMessageVue from './ShowMessage.vue' export default { components: { ShowMessageVue, HelloWorld }, data() { return { test: '测试文本', msg: '大河之剑天上来', title: '李白' } } } </script> <style scoped> h2 { color: #f40; } </style>
-
子组件如下:
<template> <h2>helloworld-根节点1</h2> <h2>helloworld-根节点2</h2> <h2>helloworld-根节点3</h2> </template> <script> export default {} </script> <style scoped></style>
-
会报一个警告,意思为不知道给那个根节点,如图:
-
可以通过
$attrs.id
手动指定绑定给那个元素
props中默认值的特殊写法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zFlBb1Cw-1689141095312)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230614102110995.png)]
子传父
通过 $emit 触发事件
什么情况下子组件需要传递内容给父组件
- 当子组件有一些事件发生的时候,比如在组件中发生了点击事件,父组件需要切换内容
- 子组件有一些内容想要传递给父组件的时候
怎么实现传递
- 首先需要在
子组件中定义好在某些情况下触发的事件名称
- 其次在
父组件中已 v-on 的方式传入要监听的事件名称
,并且绑定到对应的方法 - 最后在子组件中发生某个事件的时候,
根据事件名称触发对应的事件
子传父–数组写法
-
在子组件中,如下:
<template> <div> <!-- 绑定事件 --> <button @click="decrease">-1</button> <button @click="increase">+1</button> </div> </template> <script> export default { // 数组写法,添加需要触发的事件 emits: ['add', 'sub'], methods: { decrease() { // 通过 $emit 触发方法,第一个参数为方法名 this.$emit('sub') }, increase() { this.$emit('add') } } } </script> <style scoped></style>
-
在父组件中,如下:
<template> <div> <h2>当前计数:{ { count }}</h2> <!-- 绑定事件子组件中定义的方法,并使用一个方法回调接收 --> <count-operationVue @add="addOne" @sub="subOne"> </count-operationVue> </div> </template> <script> import CountOperationVue from './CountOperation.vue' export default { components: { CountOperationVue }, data() { return { count: 0 } }, methods: { addOne() { this.count++ }, subOne() { this.count-- } } } </script> <style scoped></style>
-
传递多个参数,添加一个输入框,和一个按钮,输入框输入多少就加多少,子组件如下:
<template> <div> <button @click="decrease">-1</button> <button @click="increase">+1</button> <input type="text" v-model.number="val" /> <button @click="increaseN">+n</button> </div> </template> <script> export default { emits: ['add', 'sub', 'addN'], data() { return { val: 0 } }, methods: { increaseN() { // 第二个参数可以携带参数,也可以通过逗号分隔传递多个参数 this.$emit('addN', this.val) }, decrease() { this.$emit('sub') }, increase() { this.$emit('add') } } } </script> <style scoped></style>
-
父组件如下:
<template> <div> <h2>当前计数:{ { count }}</h2> <count-operationVue @add="addOne" @addN="addNNum" @sub="subOne"></count-operationVue> </div> </template> <script> import CountOperationVue from './CountOperation.vue' export default { components: { CountOperationVue }, data() { return { count: 0 } }, methods: { // 可以通过形参接收传递参数 addNNum(val) { this.count += val }, addOne() { this.count++ }, subOne() { this.count-- } } } </script> <style scoped></style>
子传父-对象写法
对象写法是为了对传递的参数做验证
-
在子组件中,如下:
<template> <div> <button @click="decrease">-1</button> <button @click="increase">+1</button> <input type="text" v-model.number="val" /> <button @click="increaseN">+n</button> </div> </template> <script> export default { // emits: ['add', 'sub', 'addN'], emits: { add: null, // null 表示不需要验证 sub: null, addN: val => { console.log(val) // 如果数值小于10则不符合规则 if (val < 10) { // 不符合规则则返回 false,也会传递过去,不过会报一个警告 return false } return true } // 如果具备多个参数,逗号隔开即可 // addN: (v1, v2, v3) => {} }, data() { return { val: 0 } }, methods: { increaseN() { this.$emit('addN', this.val) }, decrease() { this.$emit('sub') }, increase() { this.$emit('add') } } } </script> <style scoped></style>
-
在父组件中,如下:
<template> <div> <h2>当前计数:{ { count }}</h2> <count-operationVue @add="addOne" @addN="addNNum" @sub="subOne"></count-operationVue> </div> </template> <script> import CountOperationVue from './CountOperation.vue' export default { components: { CountOperationVue }, data() { return { count: 0 } }, methods: { // 可以通过形参接收传递参数 addNNum(val) { this.count += val }, addOne() { this.count++ }, subOne() { this.count-- } } } </script> <style scoped></style>
-
展示结果如图:
非父子组件直接的通信
主要有两种方式:
- Provide/Inject
- Mitt全局事件总线
Provide/Inject
什么是 Provide/Inject
-
有一些
深度嵌套
的组件,子组件需要获取父组件的部分内容 -
如果这时候使用
props
沿着组件链式传递就会非常麻烦 -
如图:
-
父组件 A 获取 子组件 B 的数据时,使用 props 链式传递就很麻烦,这时候可以使用 Provide 和 Inject
- 无论层级结构有多深,父组件都可以作为所有子组件的
依赖提供者
- 父组件中有一个
provide
选项来提供数据 - 子组件中有一个
inject
选项来开始使用这些数据
- 无论层级结构有多深,父组件都可以作为所有子组件的
基础使用
-
假设我们的组件结构如图:
-
在 App 中引入 Home,在 Home 中引入 HomeContent,展示效果如图:
-
引入组件没有问题,现在在 App 中设置
provide
属性提供给子组件需要共享的数据,如下:<template> <div> <h2>App</h2> <Home></Home> </div> </template> <script> import Home from './Home.vue' export default { components: { Home }, data() { return {} }, // 提供数据 provide: { name: '张三', age: 18 }, methods: {} } </script> <style scoped></style>
-
HomeContent 如下:
<template> <!-- 使用 inject 注入的数据 --> <div>HomeContent-{ { name }}-{ { age }}</div> </template> <script> export default { // 注入父组件提供的数据 inject: ['name', 'age'] } </script> <style scoped></style>
-
输出结果如图:
-
如果给 Home 组件 provide 中设置和 App 一样的属性,就会覆盖掉 App 提供的属性,Home 如下:
<template> <div>Home</div> <home-content></home-content> </template> <script> import HomeContent from './HomeContent.vue' export default { components: { HomeContent }, provide: { name: '张三-Home', // 与 App 组件同名属性 age: 20, // 与 App 组件同名属性 address: '长沙' // 与 App 组件非同名属性 } } </script> <style scoped></style>
-
输出结果如图:
provide使用data中的数据
-
provide一般都写成函数,比如现在 APP 组件中的 data 中有一个电影列表,如果需要使用
provide
需要将 data 中的数据提供给子组件,需要改为函数写法,如下:<template> <div> <h2>App</h2> <home></home> </div> </template> <script> import Home from './Home.vue' export default { components: { Home }, data() { return { movieList: ['火星救援', '帝皇侠大电影', '肖生克的救赎', '海上钢琴师'] } }, provide() { return { name: '张三', age: 18, movies: this.movieList } }, methods: {} } </script> <style scoped></style>
-
子组件 HomeContent 如下:
<template> <!-- 使用 inject 注入的数据 --> <div>HomeContent-{ { name }}-{ { age }}-{ { address }}</div> <h2>电影列表</h2> <p v-for="(m, i) in movies" :key="i"> { { m }} </p> </template> <script> export default { // 注入父组件提供的数据 inject: ['name', 'age', 'address', 'movies'] } </script> <style scoped></style>
-
输出结果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CE181jcN-1689141095314)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230613231346423.png)]
Mitt全局事件总线
Vue3 中移除了 $on $off $once 方法,因此如果想继续使用
全局事件总线
需要通过第三方库
-
安装 mitt
-
封装 mitt,改成一个工具文件 eventbus.js
-
发送数据的组件使用 emit 方法发送事件,如下:
<template> <div> <button @click="btnClick">About点击</button> </div> </template> <script> // 引入 eventbus import emitter from './eventbus.js' export default { methods: { btnClick() { console.log('About点击了') // 使用 emmiter 对象的 emit 方法发送一个事件并携带参数 emitter.emit('jc', { name: 'zs', age: 18 }) } } } </script> <style scoped></style>
-
接收数据的组件在声明周期中使用 on 方法监听事件,如下:
<template> <div></div> </template> <script> import emitter from './eventbus.js' export default { created() { // 在生命周期内使用 on 方法监听 emit 发送的事件,回答函数接收参数 emitter.on('jc', data => { console.log('执行jc事件回调, 接收参数:', data) }) // * 表示监听所有事件 emitter.on('*', (...args) => { console.log(args) }) } } </script> <style scoped></style>
-
mitt 事件取消,在某些情况下我们希望取消掉之前注册的函数监听,两种办法:
-
取消 emitter 所有事件
emitter.all.clear()
-
取消指定的事件
function foo(){ } emitter.on('foo', onFoo) // 监听 emiiter.off('foo', onFoo) // 取消监听
-
插槽
什么是插槽
-
在开发中我们会封装一个个可复用的组件
-
我们会通过 props 传递一些数据,让组件进行展示
-
但是为了让这个组件
具备更强的通用性
,我们不能将组件中内容限制为固定的 div、span 等元素 -
比如某些情况下使用这个组件,我们希望显示的是一个按钮,某些情况下我们使用组件时希望是一张图片等
-
我们可以让
使用者决定某一个块区域
到底存放什么内容和结构 -
比如定制一个
通用的导航组件-NavBar
,这个组件分成三块区域:左 - 中 - 右,每块区域显示的内容是不固定的,如图:
如何使用插槽
如何定义插槽
- 插槽的使用过程就是
抽取共性、预留不同
- 将
共同的元素、内容
依然在组件内封装 - 将
不同的元素使用 slot 占位
,让外部决定到底显示什么样的元素
如何使用 slot
- 在 Vue 中将
<slot> 元素作为承载分发内容
的出口 - 封装组件时,使用
特殊元素<slot>
就可以为封装的组件开启一个插槽 - 该插槽
插入什么内容取决于父组件
如何使用
基础使用
-
子组件内部如下:
<template> <div> <h2>组件开始</h2> <!-- 设置一个插槽接收父组件传递的dom结构 --> <slot></slot> <h2>组件结束</h2> </div> </template> <script> export default {} </script> <style scoped></style>
-
父组件如下:
<template> <div> <my-slot-cpn> <div class="box"> <button>插入一个按钮</button> </div> </my-slot-cpn> <my-slot-cpn> <div class="box"> <p>我是普通的文本</p> </div> </my-slot-cpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue' export default { components: { MySlotCpn } } </script> <style scoped> .box { width: 200px; padding: 20px; background-color: #f40; border-radius: 4px; } </style>
-
效果显示如图:
-
也可以插入一个自定义组件,新建自定义组件文件 MyButton.vue,如下:
<template> <div> <button>coder jc button</button> </div> </template> <script> export default {} </script> <style scoped></style>
-
在父组件中导入,如下:
<template> <div> <my-slot-cpn> <div class="box"> <button>插入一个按钮</button> </div> </my-slot-cpn> <my-slot-cpn> <div class="box"> <p>我是普通的文本</p> </div> </my-slot-cpn> <my-slot-cpn> <!-- 插入自定义组件 --> <my-button></my-button> </my-slot-cpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue' import MyButton from './MyButton.vue' export default { components: { MySlotCpn, MyButton } } </script> <style scoped> .box { width: 200px; padding: 20px; background-color: #f40; border-radius: 4px; } </style>
-
显示效果如图:
插槽的默认内容
-
当一个插槽组件被使用时,如果父组件传递了内容就使用传递的,如果没有就会展示默认的信息,只需要在插槽组件中的
<slot>
元素中书写自己默认展示的内容的即可,如下:<template> <div> <h2>组件开始</h2> <slot> <!-- 设置插槽的默认内容 --> <h3 style="color: #42a5f5">默认内容展示</h3> </slot> <h2>组件结束</h2> </div> </template> <script> export default {} </script> <style scoped></style>
-
父组件如下:
<template> <div> <my-slot-cpn> <div class="box"> <button>插入一个按钮</button> </div> </my-slot-cpn> <hr /> <!-- 不在父组件中插入结构 --> <my-slot-cpn></my-slot-cpn> <hr /> <my-slot-cpn> <!-- 插入自定义组件 --> <my-button></my-button> </my-slot-cpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue' import MyButton from './MyButton.vue' export default { components: { MySlotCpn, MyButton } } </script> <style scoped> .box { width: 200px; padding: 20px; background-color: #f40; border-radius: 4px; } </style>
-
输出结果如图:
多个元素的情况
-
插槽组件如下:
<template> <div> <h2>组件开始</h2> <slot> <!-- 设置插槽的默认内容 --> <h3 style="color: #42a5f5">默认内容展示</h3> </slot> <h2>组件结束</h2> </div> </template> <script> export default {} </script> <style scoped></style>
-
父组件如下:
<template> <div> <my-slot-cpn> <!-- 插入多个元素 --> <div><button>一个按钮</button></div> <span>文本信息</span> </my-slot-cpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue' import MyButton from './MyButton.vue' export default { components: { MySlotCpn, MyButton } } </script> <style scoped></style>
-
输入结果如图:
多个插槽的情况
-
当我们插槽组件具备多个插槽时,如下:
<template> <div> <h2>组件开始</h2> <!-- 插槽一 --> <slot> <h3 style="color: #42a5f5">默认内容展示</h3> </slot> <!-- 插槽二 --> <slot> <h3 style="color: #42a5f5">默认内容展示</h3> </slot> <!-- 插槽三 --> <slot> <h3 style="color: #42a5f5">默认内容展示</h3> </slot> <h2>组件结束</h2> </div> </template> <script> export default {} </script> <style scoped></style>
-
父组件中,如下:
<template> <div> <hr /> <!-- 插入多个结构 --> <my-slot-cpn> <h2>111</h2> <span>xxxxx</span> <h2>222</h2> </my-slot-cpn> </div> </template> <script> import MySlotCpn from './MySlotCpn.vue' import MyButton from './MyButton.vue' export default { components: { MySlotCpn, MyButton } } </script> <style scoped> .box { width: 200px; padding: 20px; background-color: #f40; border-radius: 4px; } </style>
-
输出结果如图:
-
可以看到当插槽组件内部具备多个插槽时,比如这里是三个,那么外面出入的结构就会渲染三份
具名插槽
给 slot 元素添加 name 属性并指定 name 的值,这个值就是此插槽的名称;
一个不带 name 的slot,会带有隐含的名字 default;
-
插槽组件如下:
<template> <div class="nav-bar"> <!-- 左侧 --> <div class="left"> <!-- 设置名称 --> <slot name="left"></slot> </div> <!-- 中间 --> <div class="center"> <slot name="center"></slot> </div> <!-- 右侧 --> <div class="right"> <slot name="right"></slot> </div> </div> </template> <script> export default {} </script> <style scoped> .nav-bar { display: flex; align-items: center; height: 44px; } .left, .right { width: 80px; height: 100%; background-color: red; display: flex; justify-content: center; align-items: center; } .center { flex: 1; height: 100%; background-color: blue; display: flex; justify-content: center; align-items: center; } </style>
-
父组件如下:
<template> <div> <nav-bar> <!-- 使用 template 标签包裹并通过语法 v-slot:插槽名称 即可插入到对应的插槽--> <template v-slot:left> <button>左边</button> </template> <template v-slot:center> <button>中间</button> </template> <template v-slot:right> <button>右边</button> </template> </nav-bar> </div> </template> <script> import NavBar from './NavBar.vue' export default { components: { NavBar } } </script> <style scoped> .box { width: 200px; padding: 20px; background-color: #f40; border-radius: 4px; } button { padding: 5px 15px; } </style>
动态具名插槽
父组件使用子组件是传递一个值,子组件通过 props 接收,接收的这个名称可以动态绑定给内部的插槽
-
父组件如下:
<template> <div> <!-- 传入属性 name --> <demo-cpn :name="name"> <!-- 动态的使用插槽名称 --> <template v-slot:[name]> <h1>哈哈哈</h1> </template> </demo-cpn> </div> </template> <script> import DemoCpn from './DemoCpn.vue' export default { components: { DemoCpn }, data() { return { name: 'jc' } } } </script> <style scoped></style>
-
子组件如下:
<template> <div> <slot :name="name"></slot> </div> </template> <script> export default { props: { name: String } } </script> <style scoped></style>
-
也可以换成列表循环渲染,父组件如下:
<template> <div> <!-- 传入属性 name --> <demo-cpn :name="name"> <!-- 动态的使用插槽名称 --> <template v-for="(n, i) in name" :key="i" v-slot:[n]> <h1>哈哈哈{ { n }}</h1> </template> </demo-cpn> </div> </template> <script> import DemoCpn from './DemoCpn.vue' export default { components: { DemoCpn }, data() { return { name: ['jc', 'jc1', 'jc2'] } } } </script> <style scoped></style>
-
子组件如下:
<template> <div> <slot v-for="(n, i) in name" :key="i" :name="n"></slot> </div> </template> <script> export default { props: { name: String } } </script> <style scoped></style>
使用插槽时的简写
◼ 具名插槽使用的时候缩写:
跟 v-on 和 v-bind 一样,v-slot 也有缩写;
即把参数之前的所有内容 (v-slot:) 替换为字符 #;
在父组件中通过 template 标签添加插槽名称时是 v-slot:name,可以缩写成
#name
<template #jc>
<h1>哈哈哈</h1>
</template>
渲染作用域
-
在 Vue 中有渲染作用域的概念
-
父级模块里的所有内容都是
在父级作用域中编译
的 -
子模板里的所有内容都是
在子作用域中编译
的 -
如图:
-
因为组件之间都是存在
编译作用域的
,你在子组件中有没有 title 他不会去管,只会检测你当前组件中有没有 title 这个属性,没有就报警告
认识作用域插槽
-
上面的编译作用域我们知道无法直接使用子组件中的数据
-
但是如果我们在使用插槽组件的时候,给这个子组件传递了一个数组数据,然后这个子组件内部定义了一个插槽,那我们在父组件中使用这个子组件的时候,传递进来的 dom 结构如何根据子组件里面的数据渲染呢
-
我们可以给 子组件设置插槽的时候绑定属性达到传递的效果,
切记不能动态绑定 :name 属性
,因为这个属性已经被使用了,所以子组件如下:<template> <div> <!-- 我们可以给这个插槽绑定动态属性,将通过 props 接收的值在传回给父组件 --> <!-- 方便父组件在外部传入的dom结构的时候可以根据数据动态渲染 --> <!-- 可以定义多个属性,传递多个值,这些属性会被打包成一个对象发送出去,这里向外传递了userName和index值 --> <template v-for="(item, index) in names" :key="item"> <slot :userName="item" :index="index"></slot> </template> </div> </template> <script> export default { props: { names: { type: Array, default: () => [] } } } </script> <style scoped></style>
-
父组件如下:
<template> <div> <children-cpn :names="names"> <!-- 因为在组件内部是一个作用域插槽,所以如果想要在父组件这里使用传递回来的值 --> <!-- 就需要使用一个 template 标签包裹,通过 v-slot="xxx" 语法获取 --> <!-- xxx 表示接收插槽传回来的值的变量接收名称,自定义合理即可,xxx就是slot传递过来的一整个对象,里面包含了userName和index数据 --> <template v-slot="xxx"> <button>{ { xxx.userName }}-{ { xxx.index }}</button> </template> </children-cpn> <hr /> <children-cpn :names="names"> <!-- 也可以使用解构赋值 --> <template #default="{ userName, index }"> <h2>{ { userName }}-{ { index }}</h2> </template> </children-cpn> </div> </template> <script> import ChildrenCpn from './ChildrenCpn.vue' export default { components: { ChildrenCpn }, data() { return { names: ['aaa', 'bbb', 'ccc'] } } } </script> <style scoped></style>
-
输出结果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2lSMVuec-1689141095317)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230614113258648.png)]
-
当然如果子组件的插槽指定名字情况下怎么编写,子组件如下:
<template v-for="(item, index) in names" :key="item"> <!-- 设置了名称jc' --> <slot name="jc" :userName="item" :index="index"></slot> </template>
-
那么外部在使用的时候就需要设置名称,如下:
<children-cpn :names="names"> <template v-slot:jc="{ userName, index }"> <h2>{ { userName }}-{ { index }}</h2> </template> </children-cpn> <children-cpn :names="names"> <!-- 简写 --> <template #jc="{ userName, index }"> <h2>{ { userName }}-{ { index }}</h2> </template> </children-cpn>
-
如果插槽没有名称就是默认 default,那么外部在使用的时候可以省略,不过是简写的时候就需要携带上 default
生命周期
什么是生命周期
- 每个组件都可能会经历从
创建、挂载、更新、卸载
等一系列的过程 - 在这个过程中的
某一个阶段
,使用者可能会想要添加属于自己的一些代码逻辑
(比如初始化数据,进行网络请求) - 而想知道目前正处在那一个过程,我们可以
使用 Vue 给我们提供的生命周期函数
生命周期函数
- 生命周期函数是
一些钩子函数
,在某个时间会被 Vue 源码内部进行回调
- 通过对生命周期函数的回调,我们可以
知道目前组件正在经历什么阶段
- 那么我们就可以在
该生命周期中编写属于自己的逻辑代码
了 - vue从mounted钩子函数开始可以获取和操作dom,此前操作DOM浏览器会报错。
生命周期的流程
-
官方释义图如下:
八个钩子函数释义
-
beforeCreate
: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。该钩子函数的执行时机是在组件实例被创建时,即在new Vue()
之后,但在挂载之前 -
created
: 在实例创建完成后被立即调用。该钩子函数在实例的数据观测 (data observer) 和 event/watcher 事件配置之后调用,但在挂载之前 -
beforeMount
: 在挂载开始之前被调用。该钩子函数的执行时机是在模板编译完成后,但在挂载之前 -
mounted
: 在挂载完成后被调用。该钩子函数的执行时机是在模板编译、挂载、渲染组件之后 -
beforeUpdate
: 在数据更新之前被调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子函数中对更新之前的状态进行操作 -
updated
: 在数据更新之后被调用,发生在虚拟 DOM 重新渲染和打补丁之后。该钩子函数的执行时机是在组件DOM更新之后 -
beforeDestroy
: 在实例销毁之前调用。该钩子函数的执行时机是在实例即将被销毁之前 -
destroyed
: 在实例销毁之后调用。该钩子函数的执行时机是在实例被销毁之后 -
以上钩子函数的执行顺序是:
beforeCreate -> created -> beforeMount -> mounted -> beforeUpdate -> updated -> beforeDestroy -> destroyed
-
Tips
:需要注意的是,activated
和deactivated
这两个钩子函数只会在使用 keep-alive 组件时才会被调用,不属于常规生命周期函数
-
<template>
<h2>message: {
{message}}-{
{counter}}</h2>
<button @click="message = 'Hello World'">修改message</button>
<button @click="counter++">+1</button>
<div>
<button @click="isShowHome = !isShowHome">显示Home</button>
<home v-if="isShowHome"></home>
</div>
</template>
<script>
import Home from "./Home.vue"
export default {
components: {
Home
},
data() {
return {
message: "Hello App",
counter: 0,
isShowHome: true
}
},
// 1.组件被创建之前
beforeCreate() {
console.log("beforeCreate")
},
// 2.组件被创建完成
created() {
console.log("created")
console.log("1.发送网络请求, 请求数据")
console.log("2.监听eventbus事件")
console.log("3.监听watch数据")
},
// 3.组件template准备被挂载
beforeMount() {
console.log("beforeMount")
},
// 4.组件template被挂载: 虚拟DOM -> 真实DOM
mounted() {
console.log("mounted")
console.log("1.获取DOM")
console.log("2.使用DOM")
},
// 5.数据发生改变
// 5.1. 准备更新DOM
beforeUpdate() {
console.log("beforeUpdate")
},
// 5.2. 更新DOM
updated() {
console.log("updated")
},
// 6.卸载VNode -> DOM元素
// 6.1.卸载之前
beforeUnmount() {
console.log("beforeUnmount")
},
// 6.2.DOM元素被卸载完成
unmounted() {
console.log("unmounted")
}
}
</script>
<style scoped>
</style>
缓存组件的生命周期
这两个生命周期依赖于 KeepAlive 组件,KeepAlive 与
activated
和deactivated
介绍可以观看文档,文档地址:KeepAlive
动态组件与keep-alive
动态组件
<template>
<div>
<button
@click="handleTab(item)"
v-for="item in btnList"
:key="item"
:class="{ active: item === currentTab }"
>
{
{ item }}
</button>
<!-- 1、动态显示组件-基本使用 -->
<!-- 需要使用一个内置的组件 component,这个组件又一个属性 is -->
<!-- 通过设置 is 的值就可以决定使用上面组件 -->
<!-- 可以直接写死为一个固定的名称,但是会有警告 -->
<!-- <component is="home"></component> -->
<!-- 推荐:一般我们需要给他绑定一个变量,即使用 v-bind 语法 -->
<!-- 可以通过动态切换变量的值实现切换组件 -->
<!-- <component :is="currentTab"></component> -->
<!-- 2、动态显示组件-传递参数 -->
<!-- 只需要给 component 传递参数即可,当前显示的组件就会收到 -->
<component
@pageClick="getPage"
name="coderjc"
:age="age"
:is="currentTab"
></component>
</div>
</template>
<script>
import Category from './pages/Category.vue'
import Home from './pages/Home.vue'
import About from './pages/About.vue'
export default {
components: { Category, Home, About },
data() {
return {
btnList: ['home', 'category', 'about'],
currentTab: 'home',
age: 20
}
},
methods: {
handleTab(item) {
this.currentTab = item
},
getPage(val) {
console.log(`${val}组件点击了`)
}
}
}
</script>
<style scoped>
button {
padding: 5px 10px;
margin-right: 10px;
}
.active {
font-weight: bold;
color: #f40;
}
</style>
keep-alive
- include - string | RegExp | Array 只有名称匹配的组件会被缓存
- exclude - string | RegExp | Array 任何名称匹配的组件都不会被缓存
- max - number | string 最多可以缓存多个组示例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁
keep-alive 的基本使用
-
现在我们有一个案例,点击上面的三个按钮,就可以切换不同的组件,about 组件点击增加按钮的时候,计数会增加,如图:
-
假设我们现在增加到 5,先切换到其他组件,在返回 about 组件就会发现这个技术又归 0 了,这是因为组件切换的时候进行了销毁,在次进入就是又创建一次组件,所以会导致数值的重置
-
这种情况不仅无法保存原有的状态,也会非常的浪费性能,所以我们可以使用
内置组件 keep-alive 来保存这个状态
-
keep-alive 的初步使用,如下:
<!-- 我们只需要使用 keep-alive 包裹动态组件标签即可 --> <keep-alive> <component @pageClick="getPage" name="coderjc" :age="age" :is="currentTab" ></component> </keep-alive>
-
效果如图:
-
可以看到并没有重置我们的计数
include - 匹配添加名称的组件
即添加了那些组件的名称,那些组件就会被进行缓存
Tip:默认情况下不需要组件有 name 属性,但是如果需要名称匹配方式生效,那么这个组件必须在内部声明 name 属性,这个 name 属性大小写敏感,尽量保持大小写一致,如下:
export default { name: 'Home' }
-
字符串写法
,如下:-
<!-- 字符串写法 --> <!-- 只缓存 Home 和 Category 组件,使用逗号分割 --> <keep-alive include="Home,Category"> <component @pageClick="getPage" name="coderjc" :age="age" :is="currentTab" ></component> </keep-alive>
-
现在缓存了 Home 与 Category 组件,来看一下效果展示,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLCypr3u-1689141095318)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/2.gif)]
-
-
正则表达式写法
,记得添加 v-bind,否则会被当初字符串,如下:-
<!-- 正则表达式写法 --> <!-- 只缓存 Home 和 Category 组件,使用逗号分割 --> <keep-alive :include="/Home|Category/"> <component @pageClick="getPage" name="coderjc" :age="age" :is="currentTab" ></component> </keep-alive>
-
-
数组写法
,如下:-
<!-- 数组写法 --> <!-- 只缓存 Home 和 About 组件,使用逗号分割 --> <keep-alive :include="['Home' ,'About']"> <component @pageClick="getPage" name="coderjc" :age="age" :is="currentTab" ></component> </keep-alive>
-
效果如下:
-
exclude - 排除添加名称的组件
即那些组件名称被添加了,那些组件就不会被加入到缓存
三种写法与 include 一致, exclude 就只演示数组写法,其余写法参考 include
-
代码如下:
<!-- exclude --> <!-- 排除 About 组件 --> <KeepAlive :exclude="['About']"> <component @pageClick="getPage" name="coderjc" :age="age" :is="currentTab" ></component> </KeepAlive>
-
效果如图:
max - 最大缓存组件实例数量
这个一般很少使用,假设我们现在设置了 max = 5,我们设置了 5 个,如下:
<KeepAlive :include="[ 'A', 'B', 'C', 'D', 'F' ]"> <component :is="view"></component> </KeepAlive>
现在如果在添加一个 E 组件,那们就会把上面 5 个组件中最长时间没有访问的移除掉,然后添加新的组件进去
keep-alive 的缓存生命周期
是生命是生命周期和生命周期的释义翻看文档:生命周期
-
目前我们缓存的组件是 ‘Home’, ‘About’,因此可以进行测试,测试一下看看是否真的不会销毁组件,在 About 组件进行改造,增加生命周期钩子函数,如下:
<template> <div @click="handleClick">About组件:{ { name }}-{ { age }}</div> <button @click="count++">增加</button> <span> 计数:{ { count }}</span> </template> <script> export default { name: 'About', props: { name: { type: String, default: '' }, age: { type: Number, default: 0 } }, data() { return { count: 0 } }, emits: ['pageClick'], methods: { handleClick() { this.$emit('pageClick', 'about') } }, // 实例初始化之后 beforeCreate() { console.log('beforeCreate') }, // 实例创建完成 created() { console.log('created') }, // 实例销毁之前 beforeDestroy() { console.log('beforeDestroy') }, // 实例销毁完成 destroyed() { console.log('destroyed') } } </script> <style scoped></style>
-
效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVlN7f1r-1689141095319)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/ezgif.com-video-to-gif.gif)]
-
通过上图可以看出在切换组件时 beforeCreate 和 created 只执行了一次,beforeDestroy 和 destroyed 函数也没有执行,这就证明组件被缓存了没有被销毁
-
但是我们可能需要在离开这个被缓存的组件时候来执行一些代码逻辑,我们就可以利用 activated 和 deactivated 生命周期函数
-
activated
:进入被缓存的组件页面时触发 -
deactivated
:离开被缓存的组件页面时触发 -
我们可以将其添加进 About 组件测试,如下:
// 组件激活时 activated() { console.log('activated') }, // 组件冻结时(即离开组件) deactivated() { console.log('deactivated') }
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F9lUM48t-1689141095319)(C:/Users/HP/Desktop/%E9%A3%8E%E4%BD%AC%E7%AC%94%E8%AE%B0/Vue3(coderwhy)]/%E7%AC%94%E8%AE%B0/02_Vue%E7%BB%84%E4%BB%B6%E5%8C%96%E5%BC%80%E5%8F%91%E6%A8%A1%E5%9D%97/07_%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E4%B8%8Ekeep-alive.assets/111.gif)
r e f − ref- ref−parent-$root
$refs
- 在某些情况下我们需要直接获取元素对象或者子组件实例
- 在 Vue 中不推荐操作 dom,但是如果需要我们可以
给元素或者组件绑定一个 ref 属性
,然后我们可以使用 $refs 获取
- $refs 是一个对象,包含所有通过 ref 绑定的元素或者组件
绑定元素
-
代码如下:
<template> <div> <!-- 绑定到元素身上 --> <!-- 使用 ref 属性,自定义属性名 --> <h2 ref="title">哈哈哈</h2> <button @click="btnClick">获取元素</button> </div> </template> <script> export default { methods: { btnClick() { // 通过 this.$refs[ref绑定的属性名即可获取] console.log(this.$refs.title) } } } </script> <style scoped></style>
-
效果如图:
绑定组件
-
新建一个组件 DemoCpn.vue 代码如下:
<template> <div> <h2 style="color: #f40">{ { msg }}</h2> </div> </template> <script> export default { data() { return { msg: 'hello world' } }, methods: { hello() { console.log('我是 DemoCpn 组件中 hello 方法,本次传递的值是:', { msg: this.msg }) } } } </script> <style scoped></style>
-
App.vue 代码如下:
<template> <div> <!-- 绑定到组件上 --> <demo-cpn ref="demoCpn"></demo-cpn> <button @click="btnClick">获取元素</button> </div> </template> <script> // 引入 DemoCpn 组件 import DemoCpn from './DemoCpn.vue' export default { components: { DemoCpn }, methods: { btnClick() { console.log(this.$refs.demoCpn) } } } </script> <style scoped></style>
-
来看一下绑定 ref 之后的输出结果,效果如图:
-
获取的时候一个组件的实例,竟然是这个组件的实例我们就可以利用获取 DemoCpn 组件的数据,比如 data 里面的数据,我们获取一下,看看能否打印 DemoCpn 组件里面的 msg 数据,代码如下:
// 只需要改变方法里面的打印语句即可 btnClick() { console.log(this.$refs.demoCpn.msg) }
-
结果如图:
-
也因此我们可以使用 DemoCpn 组件的方法,并且利用方法传值,代码如下:
btnClick() { this.$refs.demoCpn.hello() }
-
效果如图:
$parent 与 $root
在子组件中获取父组件的实例根组件的实例
-
需要对 DemoCpn 组件代码进行改造,如下:
<template> <div> <h2 style="color: #f40">{ { msg }}</h2> <!-- 获取父组件和根组件 --> <button @click="getParentAndRoot">获取父组件和根组件</button> </div> </template> <script> export default { data() { return { msg: 'hello world' } }, methods: { hello() { console.log('我是 DemoCpn 组件中 hello 方法,本次传递的值是:', { msg: this.msg }) }, getParentAndRoot() { // 获取父组件 const parent = this.$parent console.log('父组件', parent) // 获取根组件 const root = this.$root console.log('根组件', root) } } } </script> <style scoped></style>
-
效果如图:
异步组件的使用
Webpack 的代码分包
默认的打包过程
-
在常规条件下进行打包,webpack 会将所有依赖的包进行一个打包,可以看一下我们现在的目录,如图:
-
我们先来看一下打包的结果,如图:
-
这个就是
默认打包
的结果,因为在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖
的,那么webpack 在打包时就会将所有依赖的组件模块打包到一起
,例如上方的就全都在一个 app.js 文件 -
此时我们的示例中只有一个依赖的组件,但是
当这个项目不断扩大
,app.js 文件内容就会过于庞大,而当浏览器加载一个越来越大的 app.js 文件时,就会造成首屏的渲染速度变慢
-
第三方的依赖包就会被打包进
chunk-vendors
文件中,比如:vue.js vue-router … -
因此针对这种情况,我们就会需要进行一个分包操作
分包
-
我们在上方的案例中有一个 math.js 的文件,文件内容如下:
export const sum = function (a, b) { return a + b }
-
我们在 main.js 文件中引入,如下:
import { createApp } from 'vue' import App from './13_异步组件的使用/App.vue' // 直接导入 math 文件的 sum 方法 import { sum } from './13_异步组件的使用/utils/math' console.log(sum(10, 20)) createApp(App).mount('#app')
-
而这种
直接导入的方式
,会把 math.js 文件也打包到到我们的 app.js 这个文件中,因此我们需要这种 math.js 文件在打包的时候进行分包
-
在 webpack 中,针对这种情况给出的解决方案中,一般有一种方式比较常见,
使用一个 import() 函数
导入,而这个函数的返回值是一个promise
,如下:import('./13_异步组件的使用/utils/math').then(res => { res.sum(20, 30) })
-
只要通过
import() 函数
进行导入,webpack 在打包的时候就会进行分包的操作
,打包结果如图: -
可以看到我们的打包结果已经不止一个 app.js 的包了,而是存在一个新的 js 文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BE76JKFs-1689141095322)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230615125635383.png)]
Vue 中实现异步组件
-
我们新增一个组件
asyncDemo.vue
,为了方便查看打包结果,我们取消对上面 math.js 文件的依赖,我们需要把 asyncDemo 组件写成一个异步组件,asyncDemo 组件内容如下:<template> <div> <h2>{ { msg }}</h2> </div> </template> <script> export default { data() { return { msg: 'hello world' } } } </script> <style scoped></style>
-
如果在我们使用 import asyncDemo from ‘path’ 导入的话,还是会被打包到 app.js 文件中,影响我们的打包速度,因此我们需要进行一个分包,依次来加速首屏的渲染速度
-
那么我们就可以对组件进行
异步加载
(目的即为了实现分包),在 vue 中给我们提供了一个函数,defineAsyncComponent,通过这个函数就可以实现异步加载组件,defineAsyncComponent 方法接收两种类型的参数:- 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象
- 类型二:接受一个对象类型,对异步函数进行配置
-
针对类型一,我们可以传入一个箭头函数,他需要返回一个 promise 对象,恰好在 webpack 中的 import 函数返回值就是 promise 对象,写法如下:
const asyncDemo = defineAsyncComponent(() => import('./asyncDemo.vue'))
-
在 Vue 中使用完整写法如下:
<template> <div>App组件</div> <home></home> <!-- 使用还是正常使用 --> <async-demo></async-demo> </template> <script> // 导入 defineAsyncComponent 方法 import { defineAsyncComponent } from 'vue' import Home from './Home.vue' // import asyncDemo from './asyncDemo.vue' // 使用 defineAsyncComponent 方法引入 asyncDemo 组件 const asyncDemo = defineAsyncComponent(() => import('./asyncDemo.vue')) export default { components: { Home, asyncDemo // 直接注册即可 } } </script> <style scoped></style>
-
现在可以看一下输出效果,是否这个组件会被打包到单独的文件中,如图:
-
类型二的异步组件写法如下:
// 写法二: const asyncDemo = defineAsyncComponent({ loader: () => import('./asyncDemo.vue') })
-
我们可以设置一个占位组件,LodingCpn.vue,如下:
<template> <div>加载中...</div> </template> <script> export default {} </script> <style scoped></style>
-
当
加载这个异步组件
的时候,这个组件还没被加载出来
时,可以设置一个占位的组件
,没加载组件出来之前可以显示这个占位的组件
,如下:// 引入占位的组件 import LodingCpn from './LodingCpn.vue' // 使用 const asyncDemo = defineAsyncComponent({ loader: () => import('./asyncDemo.vue'), // 当异步加载组件的时候,这个组件还没被加载出来时,可以设置一个占位的组件,没加载组件出来之前可以显示这个占位的组件 loadingComponent: LodingCpn })
-
类型二的写法还可以有一些属性,如下:
const asyncDemo = defineAsyncComponent({ loader: () => import('./asyncDemo.vue'), // 当异步加载组件的时候,这个组件还没被加载出来时,可以设置一个占位的组件,没加载组件出来之前可以显示这个占位的组件 loadingComponent: LodingCpn, // 加载异步组件失败时显示的组件 // errorComponent, // 有时候可能不希望直接显示占位的组件,需要等一定时间后在显示这个占位的组件,就可以设置延时 delay: 3000, /** * 加载异步组件失败的监听函数 * @param {*} error 错误信息 * @param {*} retry 函数,调用这个函数重新加载 * @param {*} attempts 记录重新加载的次数 */ onError: (error, retry, attempts) => { } })
-
展示一下实现的效果,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkFyJrMZ-1689141095323)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230615125342456.png)]
组件与 Suspense 结合使用
Suspense 是一个内置的全局组件,该组件也可以实现异步组件没加载出来时显示其他组件,该组件有两个插槽:
- default(默认):如果 default 可以展示,就显示 default 内容,即如果如果显示的异步组件加载出来了就使用异步组即如果
- fallback(应急):如果 default 无法显示,就显示 fallback 插槽的内容,即如果 default 上的异步组件没有被加载出来就可以使用 fallback 插槽的内容
-
首先引入异步组件与插槽组件,如下:
<script> import { defineAsyncComponent } from 'vue' const asyncDemo = defineAsyncComponent(() => import('./asyncDemo.vue')) export default { components: { asyncDemo // 直接注册即可 } </script>
-
在 html 结构中使用,如下:
<suspense> <!-- 真正需要展示的异步组件 # 为 v-slot 简写--> <template #default> <async-demo></async-demo> </template> <!-- 等待加载异步组件时的占位组件 --> <template #fallback> <loding-cpn></loding-cpn> </template> </suspense>
-
效果如图:
组件的v-model
在元素上绑定 v-model
v-model 的本质是实现了如下步骤
1、动态绑定 value 值
2、通过 input 事件来更新 data 中变量值
3、使用代码表达如下:
<!-- v-model 的本质就是下面写法的语法糖 --> <input :value="msg" @input="msg = $event.target.value" />
-
在元素上的使用非常简单,直接使用即可,代码如下:
<template> <div> <!-- <input v-model="msg" /> --> <h2>在元素上使用 v-model 的:{ { msg }}</h2> </div> </template> <script> export default { data() { return { msg: 'hello world' } } } </script> <style scoped></style>
-
效果如图:
在组件上绑定 v-model
绑定单个 v-model 写法一
组件上使用 v-model
1、组件上使用 v-model 默认在组件上绑定了一个属性 modelValue,因此组件内部需要一个 modelValue 属性
2、并绑定了一个事件 @update:属性名 这个事件名可以改但是不建议修改
3、@update:model-value=“msg = $event” 表示从组件内部将修改的新值在发送回来,并使用 $event 接收并重新赋值给 msg
4、使用代码表示如下:
<jc-inpu :modelValue="msg" @update:model-value="msg = $event"></jc-inpu>
-
具体实现还是具备很多细节的,首先我们创建一个组件 JcInput.vue,组件内容如下:
<template> <div> <!-- 这里使用一个点击事件,来改变 modelValue 的值 --> <button @click="btnClick">触发@update事件</button> <!-- 使用一个标题来显示 --> <h2>JcInput组件的modelValue:{ { modelValue }}</h2> <input type="text" :value="modelValue" /> </div> </template> <script> export default { // 内部需要一个 modelValue 属性,因为组件外部使用 v-model 的时候是默认给到了一个 modelValue 的属性的 // - 这个属性的名称也可以改变,后面在介绍 props: { modelValue: { type: String || Number } }, // 发送一个 update:modelValue 事件出去 emits: ['update:modelValue'], methods:{ // 点击触发 update:modelValue 事件 btnClick() { this.$emit('update:modelValue', '123123') } } } </script> <style scoped></style>
-
在看一下外部 App.vue,如下:
<template> <div> <jc-input v-model="msg"></jc-input> <h2>jc-在App组件的msg:{ { msg }}</h2> </div> </template> <script> import JcInput from './JcInput.vue' export default { components: { JcInput }, data() { return { msg: 'hello world' } } } </script> <style scoped></style>
-
现在我们只需要看一下点击按钮的时候能否让 msg 为 123123 即可,如图:
-
通过点击 msg 的值就从最开始的 hello world 变成了 123123,这说名在点击按钮的时候触发了
btnClick
事件,同时通过 $emit 发送事件 update:modelValue,发送之后触发了外部组件使用 v-model 的时候默认绑定的 update:modelValue 方法,这个方法在开头我们提到过,会接收一个新的值,这个值会赋值给 msgcoderwhy案例
APP.vue
<template> <div class="app"> <!-- 1.input v-model --> <!-- <input v-model="message"> <input :value="message" @input="message = $event.target.value"> --> <!-- 2.组件的v-model: 默认modelValue --> <counter v-model="appCounter"></counter> <counter :modelValue="appCounter" @update:modelValue="appCounter = $event"></counter> <!-- 3.组件的v-model: 自定义名称counter --> <!-- <counter2 v-model:counter="appCounter" v-model:why="appWhy"></counter2> --> </div> </template> <script> import Counter from './Counter.vue' import Counter2 from './Counter2.vue' export default { components: { Counter, Counter2 }, data() { return { message: "Hello World", appCounter: 100, appWhy: "coderwhy" } } } </script> <style scoped> </style>
Counter.vue
<template> <div> <h2>Counter: { { modelValue }}</h2> <button @click="changeCounter">修改counter</button> </div> </template> <script> export default { props: { modelValue: { type: Number, default: 0 } }, emits: ["update:modelValue"], methods: { changeCounter() { this.$emit("update:modelValue", 999) } } } </script> <style scoped> </style>
Counter2.vue
<template> <div> <h2>Counter: { { counter }}</h2> <button @click="changeCounter">修改counter</button> <!-- why绑定 --> <hr> <h2>why: { { why }}</h2> <button @click="changeWhy">修改why的值</button> </div> </template> <script> export default { props: { counter: { type: Number, default: 0 }, why: { type: String, default: "" } }, emits: ["update:counter", "update:why"], methods: { changeCounter() { this.$emit("update:counter", 999) }, changeWhy() { this.$emit("update:why", "kobe") } } } </script> <style scoped> </style>
绑定单个 v-model 写法二
-
我们在来看一下第二个写法,上面我们传递的是一个固定的值 123123,怎么可以传入输入框输入什么值就传递什么值呢,我们只需要对 JcInput 组件内部的 input 事件做一下改造,动态绑定 value 的值时,并通过 @input 绑定一个事件实时更新,并不在需要 button 按钮来手动控制,如下:
<template> <div> <!-- 使用一个标题来显示 --> <h2>JcInput组件的modelValue:{ { modelValue }}</h2> <span>JcInput组件的输入框</span> <input type="text" :value="modelValue" @input="btnClick" /> </div> </template> <script> export default { props: { modelValue: { type: String || Number } }, emits: ['update:modelValue'], methods:{ // 点击触发 update:modelValue 事件 btnClick(event) { // event.target.value 获取当前输入框的值 this.$emit('update:modelValue', event.target.value) } } } </script> <style scoped></style>
-
App.vue 文件无需修改,效果如图:
绑定单个 v-model 写法三(计算属性)
前面的写法看起来代码比较臃肿,我们可使用计算属性的 get 和 set 方法帮助我们实现
-
JcInput 组件修改如下:
<template> <div> <h2>JcInput组件的modelValue:{ { modelValue }}</h2> <span>JcInput组件的输入框</span> <input type="text" v-model="value" /> </div> </template> <script> export default { props: { modelValue: { type: String || Number } }, emits: ['update:modelValue'], computed: { // 使用 get 和 set value: { get() { // value访问时返回 props 传入的值 return this.modelValue }, set(newValue) { // value使用 v-model 绑定,设置值的时候使用 update:modelValue 方法重新发送newValue 值出去更新外部的值,再次触发 get 方法 this.$emit('update:modelValue', newValue) } } } } </script> <style scoped></style>
-
效果如图:
-
通过演示效果之后我们可以说一下实现的原理,计算属性 value 通过 v-model 绑定在 input 元素上,而 value 的初始值就通过
get
方法获取传入 modelValue 属性的值,且 input 标签的值发生改变时就会触发计算属性 value 的set
方法, set 方法接收最新的值,通过update:modelValue
事件更新最外部组件上的<jc-input v-model="msg"></jc-input>
中 msg 的值,当 msg 的值发生变化时就会触发内部组件的 modelValue 属性的值变化,modelValue 值的变化就会因为计算属性的特性,当依赖的属性发生改变时就会触发,因此又再次更新到计算属性 value 身上,以此形成一个循环
绑定多个 v-model
-
前面所提到的给组件绑定 v-model 时候 modelValue 是默认的属性值名称,并且是可以修改的,怎么修改,语法如下:
- v-model:属性名
通过 : 分割
,并跟上属性名称
,就可以实现修改名称的效果
- v-model:属性名
-
并且通过这种写法可以实现传递多个 v-model,我们先在 App.vue 父组件中做一下修改,如下:
<template> <div> <jc-input v-model="msg" v-model:title="title" ></jc-input> <h2>在App组件的msg:{ { msg }}</h2> <h2>在App组件的title:{ { title }}</h2> </div> </template> <script> import JcInput from './JcInput.vue' export default { components: { JcInput }, data() { return { msg: 'hello world', title: '组件绑定多个 v-model' } } } </script> <style scoped></style>
-
我们在 JcInput 组件中使用同样的方法,给 title 也使用计算属性改变,看看能否实现多个 v-model 的效果,如下:
<template> <div> <span>JcInput组件的输入框 modelValue:</span> <input type="text" v-model="value" /> <br /> <span>JcInput组件的输入框 title:</span> <input type="text" v-model="titleJc" /> <hr /> </div> </template> <script> export default { // 内部需要一个 modelValue 属性 props: { modelValue: { type: String || Number }, // 设置属性值 title 接收传入进来的 title title: { type: String } }, // 增加事件update:title emits: ['update:modelValue', 'update:title'], computed: { titleJc: { get() { return this.title }, set(newValue) { this.$emit('update:title', newValue) } }, value: { get() { return this.modelValue }, set(newValue) { this.$emit('update:modelValue', newValue) } } } } </script> <style scoped></style>
-
效果如下:
Mixin
认识 Mixin
有时候组件和组件之间会存在相同的代码逻辑,我们希望对相同代码逻辑进行抽取
- 在 Vue2 和 Vue3 中都可以使用 Mixin
- Mixin 可以以一种非常灵活的方式来
分发 Vue 组件中可复用功能
- 一个 Mixin 对象可以包含
任何组件选项
- 当组件使用 Mixin 对象时,所有的
Mixin 对象的选项将会被混合进入该组件本身的选项中
基础使用
-
创建一个文件加 Mixins,在里面创建一个 js 文件 demoMixin.js,Mixin 就是一个 js 文件里面的对象,它可以书写组件中任何选项,代码如下:
// mixin 就是一个对象 export const demoMixin = { data() { return { msg: 'hello demoMixin' } }, methods: { sayHello() { console.log('hi~ me is demoMixin') } }, created() { console.log('demoMixin created 执行') } }
-
在 App 组件中引入,当然可以在任何组件引入,如下:
<template> <div> <h2>{ { title }}</h2> <p> 混入的属性 msg 的值:<span style="color: #f40">{ { msg }}</span> </p> <button @click="sayHello">触发混入的方法</button> </div> </template> <script> import { demoMixin } from './mixins/demoMixin' export default { name: 'App', components: {}, data() { return { title: 'Mixin 演示' } }, // 混入 mixins: [demoMixin] } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
-
效果如图:
Mixin 合并规则
如果 Mixin 对象中的选项和组件对象中的选项发生了冲图,Vue 会分成不同情况处理
情况一:如果是 data 函数的返回值对象
- 返回值是对象默认情况下会
进行合并
- 如果 data 返回值对象的属性发生了冲突,那么会
保留组件自身的数据
情况二:生命周期钩子函数
- 生命周期的钩子函数
会被合并到数组中
,都会被调用
情况三:值为对象的选型,例如:methods、components 和 directives,将被合并为同一个对象
- 比如都有 methods ,并都定义了方法,那么都会生效
- 如果在对象里面的定义的 key 相同,
就会使用组件中的数据
Composition API
认识 Composition API
如果需要使用 Composition API 需要使用
setup
函数
setup
就是一个组件的另外一个选项:
- 只不过这个选项强大到我们可以用它替代之间所编写的大部分其他选项
- 比如 methods、computed、watch、data、生命周期等等
setup 函数的参数
- 主要有两个参数 props 和 context
props
-
props 就是
父组件传递过来的属性会被放到这个参数 props 对象中
,如果在 setup 中需要使用父组件传递进来的属性,之间可以通过 props 参数获取 -
对于
定义 props 的类型
,还是和之前的规则一样,在 props 选项中定义 -
并且在组件中的 html 结构中也就是 template 中依然可以使用 props 中的属性
-
setup 函数
内部没有绑定 this
,setup 函数中如果需要使用 props 只能通过 props.xxx 不能通过 this.xxx 去获取 -
子组件代码如下:
<template> <div>Home 组件</div> <!-- 在 html 结构中使用还是一样的 --> <h2>{ { msg }}</h2> </template> <script> export default { name: 'Home', // 依然需要编写 prors 属性 // - 需要在这里定义传递过来的属性名、类型、默认值 props: { msg: { type: String, default: '', required: true // 必传 } }, /** * 参数一:props 父组件传递过来的属性 */ // Tips: 在 setup 中不能使用 this setup(props, context) { // 因此如果需要 props 可以之间接收传入的 props // - 获取 console.log(props.msg) } } </script> <style scoped></style>
-
父组件如下:
<template> <div> <Home msg="hahahahahaha"></Home> </div> </template> <script> import Home from './Home' export default { components: { Home }, setup() {} } </script> <style scoped></style>
coderwhy案例
<template>
<div class="app">
<!-- template中ref对象自动解包 -->
<h2>当前计数: {
{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import { ref } from 'vue'
import useCounter from './hooks/useCounter'
export default {
setup() {
// 1.定义counter的内容
// 默认定义的数据都不是响应式数据
// let counter = ref(100)
// const increment = () => {
// counter.value++
// console.log(counter.value)
// }
// const decrement = () => {
// counter.value--
// }
// const { counter, increment, decrement } = useCounter()
return {
...useCounter()
}
}
}
</script>
<style>
</style>
context
- 这个参数包含三个属性:
attrs
:所有非 props 的 attribute(例如外部传递的 clss id 等,没有绑定到 props 上的)slots
:当父组件传递过来的插槽(这个在以函数返回时会有作用)emit
:当组件内部需要发出时间需要使用 emit,不过内部绑定 this,无法使用 this.$emit
setup 函数的返回值
-
setup 函数的返回值可以在
模板 template 中被使用
-
也就是说
通过 setup 的返回值可以来替代 data 选项
-
具体的可以堪一下代码,如下:
<template> <div> <!-- <Home msg="hahahahahaha"></Home> --> <h2>{ { title }}</h2> <p>{ { content }}</p> </div> </template> <script> import Home from './Home' export default { components: { Home }, // setup 函数的返回值 setup() { // 想要达到 data 的效果,这里必须要返回一个 对象 return { title: 'hello world', content: '欢迎来到王者荣耀' } } } </script> <style scoped></style>
-
效果如图:
-
tip:如果 setup 返回值和 data 选项中都定义了同一个属性,setup 优先级大于 data 选项,这是在 vue 中如果发现需要取到的数据在 setup 中获取到了就不会在去 data 里面找
-
利用返回值可以返回一个方法,比如我们现在有个属性 count,需要实现点击一个按钮可以实现这个 count 自增,代码如下:
<template> <div> <!-- <Home msg="hahahahahaha"></Home> --> <h2>{ { title }}</h2> <p>{ { content }}</p> <!-- 使用 count 属性 --> <h2 style="color: #f40">计数:{ { count }}</h2> <!-- 直接使用 increase 方法 --> <button @click="increase">+1</button> </div> </template> <script> import Home from './Home' export default { components: { Home }, // setup 函数的返回值 setup() { // 在 setup 函数内部定义变量 count let count = 100 // 在 setup 函数内部定义方法 increase const increase = () => { count++ console.log('increase', count) } // 想要达到 data 的效果,这里必须要返回一个 对象 return { title: 'hello world', content: '欢迎来到王者荣耀', increase, // 同时需要返回定义好的方法 increase count // 返回经过计算后的 count } } } </script> <style scoped></style>
-
如果此时点击,会发现 increase 方法触发了, count 也改变了,但是值不会发生变化,因为这时候的 count 只是一个普通的属性,并不是一个响应式的属性,可以看一下效果,如图:
Reactive API 实现响应式数据
-
上述的一个案例中我们的 count 属性因为不是响应式所以导致数据更新,但是页面显示无法更新
-
所以我们这里可以
使用 reactive 方法来让它变成响应式
-
我们需要引入这个方法,如下:
import { reactive } from 'vue'
-
将需要实现响应式的数据使用 reactive 进行包裹,如下:
<template> <div> <h2 style="color: #f40">计数:{ { state.count }}</h2> <button @click="increase">+1</button> </div> </template> <script> import { reactive } from 'vue' export default { setup() { const state = reactive({ count: 100 }) const increase = () => { state.count++ console.log('increase', state.count) } return { increase, // 返回的 count 也应该改变,可以直接返回 state,然后在模板中时通过 state 使用即可 // - 也可以直接返回 count,例如: // count:state.count state } } } </script> <style scoped></style>
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYgAtT0N-1689141095328)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/composition-API2.gif)]
Ref API
-
上述使用 reactive 的方法实现一个数据的响应式,有一些麻烦,并且使用 reactive 对传入的类型是有一些限制的,要求必须传入一个对象或者数组,如果其他类型就会进行一个警告,因此无法实现将一个简单的数值类型直接包裹,只能使用对象去实现
-
ref
会返回一个可变的响应式对象
,该对象作为一个响应式的引用
维护着它内部的值
,是通过一个 value 属性进行维护 -
使用 ref 首先需要导入,如下:
import { ref } from 'vue'
-
使用代码如下:
<template> <div> <!-- ref --> <!-- 因为是在 template 模板中,所以 vue 会自动进行解包 --> <!-- - 可以不需要使用 count.value,直接使用 count 即可 --> <h2 style="color: #f40">计数:{ { count }}</h2> <button @click="increase">+1</button> </div> </template> <script> import { ref } from 'vue' export default { setup() { // 使用 ref // - 使用 ref 之后 count 就变成了一个可响应式的引用 const count = ref(100) const increase = () => { // Tip:因为 ref 返回的是一个可维护的对象 // - 并且这个对象里面的 value 属性就是我们的值 // - 因此在使用的时候需要 属性.value count.value++ console.log('ref-increase', count.value) } return { increase, // 导出被 ref 实现响应式的数据 count // - 为什么导出的时候可以直接导出 count // - 而不需要导出 count: count.value // - 因为在 template 中直接使用 count 的时候,vue 内部会自动帮助我们解包 count } } } </script> <style scoped></style>
-
效果如图:
-
注意事项:
-
在模板中引入 ref 的值时, Vue 会自动帮助我们进行解包操作,所以我们
并不需要在模板中通过 ref.value
的方式使用 -
但是在 setup 函数内部,它依然是一个 ref引用,所以对其进行操作时,我们依然需要使用
ref.value
的方式 -
//特殊情况: <!-- 使用的时候不需要写.value --> <h2>当前计数: { { info.counter }}</h2> <!-- 修改的时候需要写.value --> <button @click="info.counter.value++">+1</button> // 3.ref是浅层解包 const info = { counter }
-
总结使用场景:
<template>
<div>
<form>
账号: <input type="text" v-model="account.username">
密码: <input type="password" v-model="account.password">
</form>
<form>
账号: <input type="text" v-model="username">
密码: <input type="password" v-model="password">
</form>
<hr>
<show-info :name="name" :age="age"></show-info>
</div>
</template>
<script>
import { onMounted, reactive, ref } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
data() {
return {
message: "Hello World"
}
},
setup() {
// 定义响应式数据: reactive/ref
// 强调: ref也可以定义复杂的数据
const info = ref({})
console.log(info.value)
// 1.reactive的应用场景
// 1.1.条件一: reactive应用于本地的数据
// 1.2.条件二: 多个数据之间是有关系/联系(聚合的数据, 组织在一起会有特定的作用)
const account = reactive({
username: "coderwhy",
password: "1234567"
})
const username = ref("coderwhy")
const password = ref("123456")
// 2.ref的应用场景: 其他的场景基本都用ref(computed)
// 2.1.定义本地的一些简单数据
const message = ref("Hello World")
const counter = ref(0)
const name = ref("why")
const age = ref(18)
// 2.定义从网络中获取的数据也是使用ref
// const musics = reactive([])
const musics = ref([])
onMounted(() => {
const serverMusics = ["海阔天空", "小苹果", "野狼"]
musics.value = serverMusics
})
return {
account,
username,
password,
name,
age
}
}
}
</script>
<style scoped>
</style>
ref获取组件实例
<template>
<!-- 1.获取元素 -->
<h2 ref="titleRef">我是标题</h2>
<button ref="btnRef">按钮</button>
<!-- 2.获取组件实例 -->
<show-info ref="showInfoRef"></show-info>
<button @click="getElements">获取元素</button>
</template>
<script>
import { ref, onMounted } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const titleRef = ref()
const btnRef = ref()
const showInfoRef = ref()
// mounted的生命周期函数
onMounted(() => {
console.log(titleRef.value)
console.log(btnRef.value)
console.log(showInfoRef.value)
showInfoRef.value.showInfoFoo()
})
function getElements() {
console.log(titleRef.value)
}
return {
titleRef,
btnRef,
showInfoRef,
getElements
}
}
}
</script>
<style scoped>
</style>
为什么在 setup 里面不能使用 this
官方对 this 的一段描述,如图:
-
表达的含义是 this 并没有指向当前组件实例
-
并且在 setup 被调用之前,data、computed、methods 等都没有被解析
-
所以无法在 setup 中获取 this
认识 readonly
-
我们通过 reactive 和 ref 可以得到一个响应式的对象,但是某些情况下,我们传入给其他地方或者其他组件的时候,可以被使用,但是不能被修改,就可以使用
readonly(只读)
方法 -
readonly 会返回原生对象的只读代理,也就是返回的也是一个 proxy 对象,只不过它的的 set 方法被劫持,并不能对其修改
-
使用方法,如下:
<template> <div> <h2 style="color: #f40">计数:{ { infoReadonly }}</h2> <button @click="increase">+1</button> </div> </template> <script> import { readonly, ref } from 'vue' export default { setup() { const count = ref(100) // 使用 readonly 包裹通过 ref 返回的响应式对象 const infoReadonly = readonly(count) console.log(infoReadonly.value) const increase = () => { // readonly 返回的是一个只读代理 // - 即 count 的 proxy 代理的 get 方法 infoReadonly.value++ console.log('readobly-increase', infoReadonly.value) } return { increase, infoReadonly } } } </script> <style scoped></style>
-
效果如图:
-
当然也可以不是一个响应式的对象,如下:
<template> <div> <h2 style="color: #f40">计数:{ { infoReadonly }}</h2> <button @click="increase">+1</button> <h2>只读属性-非响应式数据:{ { infoReadonly1.name }}</h2> <button @click="updateData">修改name的值</button> </div> </template> <script> import { readonly, ref } from 'vue' export default { setup() { const count = ref(100) // 使用 readonly 包裹通过 ref 返回的响应式对象 const infoReadonly = readonly(count) // readonly 包裹非响应式数据 const obj = { name: '张三' } const infoReadonly1 = readonly(obj) const increase = () => { // readonly 返回的是一个只读代理 // - 即 count 的 proxy 代理的 get 方法 infoReadonly.value++ console.log('readobly-increase', infoReadonly.value) } // 修改 name 的值 const updateData = () => { infoReadonly1.name = '李四' } return { updateData, increase, infoReadonly, infoReadonly1 } } } </script> <style scoped></style>
-
效果如图:
Reactive 判断的 API
这些 api 的使用都是从 vue 中导入,例如:
import { isProxy, isReactive } from 'vue'
isProxy
检查对象是否是有 reactive 或 readonly 创建的 proxy
isReactive
检查对象是否是由 reactive 创建的响应式代理
如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,他也会返回 true
isReadonly
检查对象是否有 readonly 创建的只读代理
toRow
返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用,谨慎使用)
shallowReactive
创建一个响应式代理,它跟踪器自身 property 的响应性,但不执行嵌套对象的深层响应式转换(深层还是原生对象)
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读对象(深层还是可读、可写的)
toRefs API
- 如果我们使用解构语法,对 reactive 返回的对象进行解构赋值,那么之后修改解构后的变量,
数据就不再是响应式的
- 使用 toRefs 或者 toRef 就可以让我们解构出来的变量被转成 ref 对象,实现数据的响应式
- 这种做法相当于在于 reactive 之间建立了一个链接,任何一个修改都会引起另外一个的变化
要求传入的是一个响应式对象,不能是普通的对象
-
我们可以先看一下不使用 toRefs 时直接结构会如下:
<template> <div> <h2>姓名:{ { name }}-年龄:{ { age }}</h2> <button @click="changeAge">年龄+1</button> </div> </template> <script> import { reactive } from 'vue' export default { setup() { const info = reactive({ name: '张三', age: 19 }) // 直接解构,解构出来的值不会是响应式的数据 let { name, age } = info // 修改 age const changeAge = () => { age++ console.log('直接解构-', age) } return { changeAge, name, age } } } </script> <style scoped></style>
-
效果如图:
-
可以看到数据发生改变页面数据没有变化,这时候就可以使用
toRefs
api来实现对解构出来的值也实现响应式,代码如下:<template> <div> <h2>姓名:{ { name }}-年龄:{ { age }}</h2> <button @click="changeAge">年龄+1</button> </div> </template> <script> import { reactive, toRefs } from 'vue' export default { setup() { const info = reactive({ name: '张三', age: 19 }) // 直接解构,解构出来的值不会是响应式的数据 // let { name, age } = info // 使用 toRefs // - toRefs 是一个函数。将需要解构的对象传入即可 // - 返回也是一个 ref 对象 let { name, age } = toRefs(info) // 修改 age const changeAge = () => { // 因为返回的是一个 ref 对象。所以在函数内部是使用需要使用 .value age.value++ console.log('使用 toRefs 解构-', age.value) } return { changeAge, name, age } } } </script> <style scoped></style>
-
效果如图:
-
Tips
: toRefs 是默认会把这个对象里面所有的属性都转为 ref 对象,不管你有没有在解构的时候解构它,而当你只需要使用这个对象中的某一个属性的时候,又不想全部解构造成性能的额外开销,可以使用 toRef,第一个参数是要解构的对象,第二个参数是要解构对象里的字段名 -
toRef
使用如下:// 引入 import { reactive, toRefs, toRef } from 'vue' // 使用 const info = reactive({ name: '张三', age: 19 }) // toRef // - toRef(对象, key) // - 第一个参数:传入需要解构的对象, // - 第二个参数:传入需要解构的 key // 返回的就是一个 ref 对象的值,就不在需要解构了 let name = toRef(info, 'name')
Ref 的其他 API
unref
- 如果我们需要获取一个 ref 引用中的 valu,那么也可以通过 unref 方法
如果参数是一个 ref
,则返回内部值
,否则返回参数本身
- 实际上就是一个 val = isRef(val) ? val.alue : val 的语法糖函数
isRef
判断是否是一个 ref 对象
shallowRef
创建一个浅层的 ref 对象
triggerRef
手动触发和 shallowRef 相关联的副作用,即如果浅层对象没有进行数据响应式,但是希望这些没有响应式的属性改变后可以使用 triggerRef 刷新页面
computed-watchEffect-watch
computed 在 composition-api 的使用
用法一
只传入一个 getter 函数
-
代码如下:
<template> <div> <h2>{ { fullName }}</h2> <button @click="changeName">修改name</button> </div> </template> <script> // 引入一个 computed 函数 import { ref, computed } from 'vue' export default { setup() { const firstName = ref('张') const lastName = ref('三') // 用法一:传入 getter 函数 // - computed 返回的是一个 ref 对象 const fullName = computed(() => `${firstName.value} ${lastName.value}`) const changeName = () => { firstName.value = '李' } return { changeName, fullName } } } </script> <style scoped></style>
-
看一下能否实现效果,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNSp44we-1689141095330)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/computed%E5%92%8Cwatch1.gif)]
用法二
传入对象,包含 getter/setter
-
代码如下:
<template> <div> <h2>{ { fullName }}</h2> <button @click="changeName">修改name</button> <br /> 修改整个name: <input type="text" @change="changeFullName" /> </div> </template> <script> // 引入一个 computed 函数 import { ref, computed } from 'vue' export default { setup() { const firstName = ref('张') const lastName = ref('三') // 用法一:传入 getter 函数 // const fullName = computed(() => `${firstName.value} ${lastName.value}`) // 用法二:传入对象,包含 getter/setter const fullName = computed({ get() { return firstName.value + ' ' + lastName.value }, set(value) { firstName.value = value.split(' ')[0] lastName.value = value.split(' ')[1] } }) const changeName = () => { firstName.value = '李' } const changeFullName = e => { fullName.value = e.target.value } return { changeFullName, changeName, fullName } } } </script> <style scoped></style>
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zd2MpQzm-1689141095330)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/computed%E5%92%8Cwatch2.gif)]
watchEffect
在 composition 中,我们可与使用 watchEffect 和 watch 来完成响应式数据的侦听
- 在 watchEffect 用于自动收集响应式数据的依赖
- watch 需要手动指定侦听的数据源
wathcEffect 的基本使用
-
代码如下:
<template> <div> <h2>姓名:{ { name }} - 年龄:{ { age }}</h2> <button @click="changeName">修改姓名</button> <button @click="changeAge">修改年龄</button> </div> </template> <script> import { ref, watchEffect } from 'vue' export default { setup() { const name = ref('张三') const age = ref(18) // watchEffect 会自动收集函数内部使用的依赖 // - 并立即执行一次 // - 后续依赖的值修改也都会触发 watchEffect(() => { console.log('姓名:', name.value, '年龄:', age.value) }) const changeName = () => (name.value = '李四') const changeAge = () => age.value++ return { changeAge, changeName, name, age } } } </script> <style scoped></style>
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obvVP5xN-1689141095330)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/computed%E5%92%8Cwatch3.gif)]
watchEffect 停止监听
-
如果我们有时候希望当某一个条件的情况下就停止监听,可以
利用 watchEffect 的返回值
,watchEffect 默认返回一个函数,调用这个函数就可以停止监听,如下:<template> <div> <h2>姓名:{ { name }} - 年龄:{ { age }}</h2> <button @click="changeName">修改姓名</button> <button @click="changeAge">修改年龄</button> </div> </template> <script> import { ref, watchEffect } from 'vue' export default { setup() { const name = ref('张三') const age = ref(18) // 利用返回的函数进行停止监听 const stopWatch = watchEffect(() => { console.log('姓名:', name.value, '年龄:', age.value) }) const changeName = () => (name.value = '李四') const changeAge = () => { age.value++ // 当年龄大于 22 停止监听 if (age.value > 22) { stopWatch() } } return { changeAge, changeName, name, age } } } </script> <style scoped></style>
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r80LKyf8-1689141095331)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/computed%E5%92%8Cwatch4.gif)]
watchEffect 在 setup 中获取 ref 引用的 dom
-
利用 watchEffect 在 setup 中获取 ref 引用的 dom,代码如下:
<template> <div> <!-- 使用 ref 获取 dom --> <h2 ref="title">在 setup 中使用 ref</h2> </div> </template> <script> import { ref, watchEffect } from 'vue' export default { setup() { const title = ref(null) // 可以使用生命周期和 watchEffect watchEffect(() => { console.log('获取 dom 元素:', title.value) }) return { title } } } </script> <style scoped></style>
-
效果如图:
-
出现这个情况的原因是 setup 生成的时候还没有到 dom 挂载的时候,所以第一次 watchEffect 执行的时候还是为 null,但是这个属性被收集了依赖,因此挂在完毕之后又会触发一次,就可以正常获取了
-
如果希望等到 dom 挂在完毕之后在执行这个 watchEffect,可以传入第二个参数,一个配置对象,如下:
watchEffect( () => { console.log('获取 dom 元素:', title.value) }, { // flush 具备三个属性: // - sync 强制同步(不推荐) // - pre 提前执行一次(默认值) // - post 等待 dom 挂载完毕后执行 flush: 'post' } )
-
效果如图:
-
没有在出现 null 属性了
watch
wathc 的 api 完全等同于 wathc 选项 api
- watch 需要侦听特定的数据源,并且回调函数中执行副作用
- 默认情况下他是惰性的,只有当被侦听的源发生变化时才会执行回调
watch 笔记:wathc文档地址
watch 侦听单个数据源
情况一:传入一个 getter 函数
-
代码如下:
<template> <div> <button @click="changeInfoData">修改info数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue' export default { setup() { // wathc // - 参数一:传入监听的属性 // - 参数二:传入值发生变化执行的回调 // - 参数三:配置对象 const info = reactive({ name: 'zs', age: 18 }) // 传入一个 getter 函数 watch( () => info.name, (newValue, oldValue) => { console.log('新值:', newValue, '旧值:', oldValue) } ) const changeInfoData = () => { info.name = 'jc' } return { changeInfoData } } } </script> <style scoped></style>
-
效果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GSb7KEAc-1689141095331)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230616210725893.png)]
-
获取的是一个值的本身,可以得到新旧值的变化
情况二:直接监听 info 这个 reactive 对象
-
代码如下:
<template> <div> <button @click="changeInfoData">修改info数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue' export default { setup() { const info = reactive({ name: 'zs', age: 18 }) // 直接监听 info 这个 reactive 对象 watch(info, (newValue, oldValue) => { console.log('新值:', newValue, '旧值:', oldValue) }) const changeInfoData = () => { info.name = 'jc' } return { changeInfoData } } } </script> <style scoped></style>
-
效果如图:
-
新旧值一致,返回的是一个 reactive 对象
情况三:传递一个 getter 函数,并解构
-
代码如下:
<template> <div> <button @click="changeInfoData">修改info数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue' export default { setup() { const info = reactive({ name: 'zs', age: 18 }) // 对 reactive 对象进行解构 watch( () => { return { ...info } }, (newValue, oldValue) => { console.log('新值:', newValue, '旧值:', oldValue) } ) const changeInfoData = () => { info.name = 'jc' } return { changeInfoData } } } </script> <style scoped></style>
-
效果如图:
-
返回的是一个普通对象而非 reactive 对象
情况四:obj 是一个 ref 创建响应式数据,是一个简单数据类型
-
代码如下:
<template> <div> <button @click="chanegeRefName">修改name数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue' export default { setup() { let name = ref('zs') watch(name, (newValue, oldValue) => { console.log('新值:', newValue, '旧值:', oldValue) }) const chanegeRefName = () => { name.value = '张三' } return { chanegeRefName } } } </script> <style scoped></style>
-
效果如图:
-
可以获取新旧值变化
情况五:obj 是一个 ref 对象,且传入的值是一个对象
-
代码如下:
<template> <div> <button @click="changeObjData">修改obj数据</button> </div> </template> <script> import { ref, reactive, watch } from 'vue' export default { setup() { const obj = ref({ name: 'ls', age: 20 }) watch( obj, (newValue, oldValue) => { console.log('新值:', newValue, '旧值:', oldValue) }, { deep: true } ) const changeObjData = () => { obj.value.name = 'ww' } return { changeObjData } } } </script> <style scoped></style>
-
效果如图:
-
需要开启深度监听,新旧值一致,返回的是一个 ref 响应式对象
情况六:遍历多个
只需要传入的参数改为数组即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkR65c9m-1689141095333)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230617144848023.png)]
Composition-API-生命周期函数
生命周期使用
composition api 与 options api 生命周期的概念差不多,只是使用的语法有差别
- 只是 beforeCreate、created 有一些差别,在 composition api 中不在推荐在这两个生命周期里面执行函数了
- setup 比这两个生命周期执行都要早,如果有需要代码需要在挂载前处理,可以在 setup 中处理
代码如下:
<template>
<div>
<h2>计数:{
{ count }}</h2>
<button @click="changeCount">+1</button>
</div>
</template>
<script>
import { onMounted, onUpdated, onUnmounted, ref } from 'vue'
export default {
setup() {
// 导入生命周期函数,传入一个回调
onMounted(() => {
console.log('onMounted1')
})
// 允许通过生命周期函数存在创建多个生命周期
onMounted(() => {
console.log('onMounted2')
})
onUpdated(() => {
console.log('onUpdated')
})
onUnmounted(() => {
console.log('onUnmounted')
})
const count = ref(0)
const changeCount = () => {
count.value++
}
return { count, changeCount }
}
}
</script>
<style scoped></style>
Composition provide和inject
◼ 事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
◼ 我们可以通过 provide来提供数据:
可以通过 provide 方法来定义每个 Property;
◼ provide可以传入两个参数:
name:提供的属性名称;
value:提供的属性值;
◼ 在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
可以通过 inject 来注入需要的内容;
◼ inject可以传入两个参数:
要 inject 的 property 的 name;
默认值;
***app.vue
<template>
<div>AppContent: {
{ name }}</div>
<button @click="name = 'kobe'">app btn</button>
<show-info></show-info>
</template>
<script>
import { provide, ref } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const name = ref("why")
provide("name", name)
provide("age", 18)
return {
name
}
}
}
</script>
<style scoped>
</style>
***showInfo.vue
<template>
<div>ShowInfo: {
{ name }}-{
{ age }}-{
{ height }} </div>
</template>
<script>
import { inject } from 'vue'
export default {
// inject的options api注入, 那么依然需要手动来解包
// inject: ["name", "age"],
setup() {
const name = inject("name")
const age = inject("age")
const height = inject("height", 1.88)
return {
name,
age,
height
}
}
}
</script>
<style scoped>
</style>
hooks练习
app.vue
<template>
<div>AppContent</div>
<button @click="changeTitle">修改title</button>
<!-- 1.计数器 -->
<!-- <hr>
<home></home>
<hr>
<about></about> -->
<!-- 2.home和about页面的切换 -->
<button @click="currentPage = 'home'">home</button>
<button @click="currentPage = 'about'">about</button>
<component :is="currentPage"></component>
<div class="content"></div>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
<br><br><br><br><br><br>
</template>
<script>
import { ref } from 'vue'
import Home from './views/Home.vue'
import About from './views/About.vue'
import useTitle from './hooks/useTitle'
export default {
components: {
Home,
About
},
setup() {
const currentPage = ref("home")
function changeTitle() {
useTitle("app title")
}
return {
changeTitle,
currentPage
}
}
}
</script>
<style scoped>
.content {
width: 3000px;
height: 100px;
background-color: orange;
}
</style>
home.vue
<template>
<h2>Home计数: {
{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="popularClick">首页-流行</button>
<button @click="hotClick">首页-热门</button>
<button @click="songClick">首页-歌单</button>
<div class="scroll">
<h2>x: {
{ scrollPosition.x }}</h2>
<h2>y: {
{ scrollPosition.y }}</h2>
</div>
</template>
<script>
import { onMounted, ref } from 'vue'
import useCounter from '../hooks/useCounter'
import useTitle from '../hooks/useTitle'
import useScrollPosition from '../hooks/useScrollPosition'
export default {
setup() {
// 1.counter逻辑
const { counter, increment, decrement } = useCounter()
// 2.修改标题
const { title } = useTitle("首页")
// 3.监听按钮的点击
function popularClick() {
title.value = "首页-流行"
}
function hotClick() {
title.value = "首页-热门"
}
function songClick() {
title.value = "首页-歌单"
}
// 4.获取滚动位置
const { scrollPosition } = useScrollPosition()
console.log(scrollPosition)
return {
counter,
increment,
decrement,
popularClick,
hotClick,
songClick,
scrollPosition
}
}
}
</script>
<style scoped>
</style>
about.vue
<template>
<h2>About计数: {
{ counter }}</h2>
<button @click="increment">+1</button>
<button @clcik="decrement">-1</button>
</template>
<script>
import { onActivated } from 'vue'
import useCounter from '../hooks/useCounter'
import useTitle from '../hooks/useTitle'
export default {
setup() {
// 切换标题
useTitle("关于")
return {
...useCounter()
}
}
}
</script>
<style scoped>
</style>
useCounter.js
import {
ref, onMounted } from 'vue'
export default function useCounter() {
const counter = ref(0)
function increment() {
counter.value++
}
function decrement() {
counter.value--
}
onMounted(() => {
setTimeout(() => {
counter.value = 989
}, 1000);
})
return {
counter,
increment,
decrement
}
}
useTitle.js
import {
ref, watch } from "vue";
export default function useTitle(titleValue) {
// document.title = title
// 定义ref的引入数据
const title = ref(titleValue)
// 监听title的改变
watch(title, (newValue) => {
document.title = newValue
}, {
immediate: true
})
// 返回ref值
return {
title
}
}
useScrollPositon.js
import {
reactive } from 'vue'
export default function useScrollPosition() {
// 1.使用reative记录位置
const scrollPosition = reactive({
x: 0,
y: 0
})
// 2.监听滚动
document.addEventListener("scroll", () => {
scrollPosition.x = window.scrollX
scrollPosition.y = window.scrollY
})
return {
scrollPosition
}
}
setup语法
◼ **
更少的样板内容,更简洁的代码;
能够使用纯 Typescript 声明 prop 和抛出事件;
更好的运行时性能 ;
更好的 IDE 类型推断性能 ;
◼ 使用这个语法,需要将 setup attribute 添加到
◼ 里面的代码会被编译成组件 setup() 函数的内容:
这意味着与普通的
defineProps、defineEmits、defineExpose的使用
defineProps、defineEmits、defineExpose调用函数时都是往里面传一个对象或者数组
app.vue
<template>
<div>AppContent: {
{ message }}</div>
<button @click="changeMessage">修改message</button>
<show-info name="why" :age="18" @info-btn-click="infoBtnClick" ref="showInfoRef"> </show-info>
<show-info></show-info>
<show-info></show-info>
</template>
<script setup>
// 1.所有编写在顶层中的代码, 都是默认暴露给template可以使用
import { ref, onMounted } from 'vue'
import ShowInfo from './ShowInfo.vue'
// 2.定义响应式数据
const message = ref('Hello World')
console.log(message.value)
// 3.定义绑定的函数
function changeMessage() {
message.value = '你好啊, 李银河!'
}
function infoBtnClick(payload) {
console.log('监听到showInfo内部的点击:', payload)
}
// 4.获取组件实例
const showInfoRef = ref()
onMounted(() => {
showInfoRef.value.foo()
})
</script>
<style scoped></style>
showInfo.vue
<template>
<div>ShowInfo: {
{ name }}-{
{ age }}</div>
<button @click="showInfoBtnClick">showInfoButton</button>
</template>
<script setup>
// 定义props
const props = defineProps({
name: {
type: String,
default: "默认值"
},
age: {
type: Number,
default: 0
}
})
// 绑定函数, 并且发出事件
const emits = defineEmits(["infoBtnClick"])
function showInfoBtnClick() {
emits("infoBtnClick", "showInfo内部发生了点击")
}
// 定义foo的函数
function foo() {
console.log("foo function")
}
defineExpose({
foo
})
</script>
<style scoped>
</style>
Vue-router
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdpVqncW-1689141095333)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230617215215197.png)]
后端路由阶段
◼ 早期的网站开发整个HTML页面是由服务器来渲染的.
服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
◼ 但是, 一个网站, 这么多页面服务器如何处理呢?
一个页面有自己对应的网址, 也就是URL;
URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理;
Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
◼ 上面的这种操作, 就是后端路由:
当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
◼ 后端路由的缺点:
一种情况是整个页面的模块由后端人员来编写和维护的;
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码;
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情;
前后端分离阶段
◼ 前端渲染的理解:
每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;
同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;
◼ 前后端分离阶段:
随着Ajax的出现, 有了前后端分离的开发模式;
后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中;
这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上;
并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可;
目前比较少的网站采用这种模式开发;
◼ 单页面富应用阶段:
其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
也就是前端来维护一套路由规则.
◼ 前端路由的核心是什么呢?改变URL,但是页面不进行整体的刷新。
URL的hash
◼ 前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
◼ URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新
◼ hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
HTML5的History
◼ history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:
replaceState:替换原来的路径;
pushState:使用新的路径;
popState:路径的回退;
go:向前或向后改变路径;
forward:向前改变路径;
back:向后改变路径;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nmYBLF6C-1689141095334)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618090200164.png)]
认识vue-router
◼ 目前前端流行的三大框架, 都有自己的路由实现:
Angular的ngRouter
React的ReactRouter
Vue的vue-router
◼ Vue Router 是 Vue.js 的官方路由:
它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用(SPA)变得非常容易;
目前Vue路由最新的版本是4.x版本,我们上课会基于最新的版本讲解;
◼ vue-router是基于路由和组件的
路由用于设定访问路径, 将路径和组件映射起来;
在vue-router的单页面应用中, 页面的路径的改变就是组件的切换;
◼ 安装Vue Router:
npm install vue-router
路由的使用步骤
◼ 使用vue-router的步骤:
第一步:创建路由需要映射的组件(打算显示的页面);
第二步:通过createRouter创建路由对象,并且传入routes和history模式;
✓ 配置路由映射: 组件和路径映射关系的routes数组;
✓ 创建基于hash或者history的模式;
第三步:使用app注册路由对象(use方法);
第四步:路由使用: 通过和;
路由的默认路径
◼ 我们这里还有一个不太好的实现:
默认情况下, 进入网站的首页, 我们希望渲染首页的内容;
但是我们的实现中, 默认没有显示首页组件, 必须让用户点击才可以;
◼ 如何可以让路径默认跳到到首页, 并且渲染首页组件呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcmHfibv-1689141095335)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618092916130.png)]
◼ 我们在routes中又配置了一个映射:
path配置的是根路径: /
redirect是重定向, 也就是我们将根路径重定向到/home的路径下, 这样就可以得到我们想要的结果了.
router-link
◼ router-link事实上有很多属性可以配置:
◼ to属性:
是一个字符串,或者是一个对象
◼ replace属性:
设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push(),点击返回时,不会有历史记录,返回不了;
◼ active-class属性:
设置激活a元素后应用的class,默认是自带router-link-active类的
◼ exact-active-class属性:
链接精准激活时,应用于渲染的 的 class,默认是router-link-exact-active;
路由懒加载
◼ 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载:
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效;
也可以提高首屏的渲染效率;
◼ 其实这里还是我们前面讲到过的webpack的分包知识,而Vue Router默认就支持动态来导入组件:
这是因为component可以传入一个组件,也可以接收一个函数,该函数 需要放回一个Promise;
而import函数就是返回一个Promise;
打包效果分析
◼ 我们看一下打包后的效果:
◼ 我们会发现分包是没有一个很明确的名称的,其实webpack从3.x开始支持对分包进行命名(chunk name):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-seCHQAYK-1689141095335)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618094252901.png)]
路由的其他属性
◼ name属性:路由记录独一无二的名称;
◼ meta属性:自定义的数据
{
name: "home",
path: "/home",
component: () => import("../Views/Home.vue"),
meta: {
name: "why",
age: 18
}
}
动态路由
◼ 很多时候我们需要将给定匹配模式的路由映射到同一个组件:
例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的ID是不同的;
在Vue Router中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dBa4ERb4-1689141095336)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618095639244.png)]
◼ 在router-link中进行如下跳转:
◼ 那么在User中如何获取到对应的值呢?
在template中,直接通过 $route.params获取值;
✓ 在created中,通过 this.$route.params获取值;
✓ 在setup中,我们要使用 vue-router库给我们提供的一个hook useRoute;
➢ 该Hook会返回一个Route对象,对象中保存着当前路由相关的值;
路由404
◼ 对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面
比如NotFound的错误页面中,这个时候我们可编写一个动态路由用于匹配所有的页面;
{
// abc/cba/nba
path: "/:pathMatch(.*)*",
component: () => import("../Views/NotFound.vue")
}
◼ 我们可以通过 $route.params.pathMatch获取到传入的参数:
<div class="not-found">
<h2>NotFound: 您当前的路径{
{
$route.params.pathMatch }}不正确, 请输入正确的路径!</h2>
</div>
匹配规则加*
路由的嵌套
◼ 什么是路由的嵌套呢?
目前我们匹配的Home、About、User等都属于第一层路由,我们在它们之间可以来回进行切换;
◼ 但是呢,我们Home页面本身,也可能会在多个组件之间来回切换:
比如Home中包括Product、Message,它们可以在Home内部来回切换;
这个时候我们就需要使用嵌套路由,在Home中也使用 router-view 来占位之后需要渲染的组件;
// 创建一个路由: 映射关系
const router = createRouter({
// 指定采用的模式: hash
history: createWebHashHistory(),
// history: createWebHistory(),
// 映射关系
routes: [
{
path: "/",
redirect: "/home"
},
{
name: "home",
path: "/home",
component: () => import("../Views/Home.vue"),
meta: {
name: "why",
age: 18
},
children: [
{
path: "/home",
redirect: "/home/recommend"
},
{
path: "recommend", // /home/recommend
component: () => import("../Views/HomeRecommend.vue")
},
{
path: "ranking", // /home/ranking
component: () => import("../Views/HomeRanking.vue")
}
]
},
{
name: "about",
path: "/about",
component: () => import("../Views/About.vue")
},
{
path: "/user/:id",
component: () => import("../Views/User.vue")
},
{
path: "/order",
component: () => import("../Views/Order.vue")
},
{
path: "/login",
component: () => import("../Views/Login.vue")
},
{
// abc/cba/nba
path: "/:pathMatch(.*)*",
component: () => import("../Views/NotFound.vue")
}
]
})
代码的页面跳转
替换当前的位置
◼ 使用push的特点是压入一个新的页面,那么在用户点击返回时,上一个页面还可以回退,但是如果我们希望当前页面是一个替换
操作,那么可以使用replace:
页面的前进后退
◼ router的go方法:
◼ router也有back:
通过调用 history.back() 回溯历史。相当于 router.go(-1);
◼ router也有forward:
通过调用 history.forward() 在历史中前进。相当于 router.go(1);
query方式的参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPuzOG1j-1689141095339)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618111648402.png)]
动态添加路由
◼ 某些情况下我们可能需要动态的来添加路由:
比如根据用户不同的权限,注册不同的路由;
这个时候我们可以使用一个方法 addRoute;
◼ 如果我们是为route添加一个children路由,那么可以传入对应的name:
// 1.动态管理路由
let isAdmin = true
if (isAdmin) {
// 一级路由
router.addRoute({
path: "/admin",
component: () => import("../Views/Admin.vue")
})
// 添加vip页面
router.addRoute("home", {
path: "vip",
component: () => import("../Views/HomeVip.vue")
})
}
// 获取router中所有的映射路由对象
console.log(router.getRoutes())
动态管理路由的其他方法(了解)
◼ 删除路由有以下三种方式:
方式一:添加一个name相同的路由;
方式二:通过removeRoute方法,传入路由的名称;
方式三:通过addRoute方法的返回值回调;
◼ 路由的其他方法补充:
router.hasRoute():检查路由是否存在。
router.getRoutes():获取一个包含所有路由记录的数组。
路由导航守卫
◼ vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。
◼ 全局的前置守卫beforeEach是在导航触发时会被回调的:
◼ 它有两个参数:
to:即将进入的路由Route对象;
from:即将离开的路由Route对象;
◼ 它有返回值:
false:取消当前导航;
不返回或者undefined:进行默认导航;
返回一个路由地址:
✓ 可以是一个string类型的路径;
✓ 可以是一个对象,对象中包含path、query、params等信息;
◼ 可选的第三个参数:next(不推荐使用)
在Vue2中我们是通过next函数来决定如何进行跳转的;
但是在Vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next;
// 2.路由导航守卫
// 进行任何的路由跳转之前, 传入的beforeEach中的函数都会被回调
// 需求: 进入到订单(order)页面时, 判断用户是否登录(isLogin -> localStorage保存token)
// 情况一: 用户没有登录, 那么跳转到登录页面, 进行登录的操作
// 情况二: 用户已经登录, 那么直接进入到订单页面
router.beforeEach((to, from) => {
// 1.进入到任何别的页面时, 都跳转到login页面
// if (to.path !== "/login") {
// return "/login"
// }
// 2.进入到订单页面时, 判断用户是否登录
const token = localStorage.getItem("token")
if (to.path === "/order" && !token) {
return "/login"
}
})
coderwhy源码
import {
createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// import Home from '../Views/Home.vue'
// import About from '../Views/About.vue'
// 路由的懒加载
// const Home = () => import(/* webpackChunkName: 'home' */"../Views/Home.vue")
// const About = () => import(/* webpackChunkName: 'about' */"../Views/About.vue")
// 创建一个路由: 映射关系
const router = createRouter({
// 指定采用的模式: hash
history: createWebHashHistory(),
// history: createWebHistory(),
// 映射关系
routes: [
{
path: "/",
redirect: "/home"
},
{
name: "home",
path: "/home",
component: () => import("../Views/Home.vue"),
meta: {
name: "why",
age: 18
},
children: [
{
path: "/home",
redirect: "/home/recommend"
},
{
path: "recommend", // /home/recommend
component: () => import("../Views/HomeRecommend.vue")
},
{
path: "ranking", // /home/ranking
component: () => import("../Views/HomeRanking.vue")
}
]
},
{
name: "about",
path: "/about",
component: () => import("../Views/About.vue")
},
{
path: "/user/:id",
component: () => import("../Views/User.vue")
},
{
path: "/order",
component: () => import("../Views/Order.vue")
},
{
path: "/login",
component: () => import("../Views/Login.vue")
},
{
// abc/cba/nba
path: "/:pathMatch(.*)*",
component: () => import("../Views/NotFound.vue")
}
]
})
// 1.动态管理路由
let isAdmin = true
if (isAdmin) {
// 一级路由
router.addRoute({
path: "/admin",
component: () => import("../Views/Admin.vue")
})
// 添加vip页面
router.addRoute("home", {
path: "vip",
component: () => import("../Views/HomeVip.vue")
})
}
// 获取router中所有的映射路由对象
console.log(router.getRoutes())
// 2.路由导航守卫
// 进行任何的路由跳转之前, 传入的beforeEach中的函数都会被回调
// 需求: 进入到订单(order)页面时, 判断用户是否登录(isLogin -> localStorage保存token)
// 情况一: 用户没有登录, 那么跳转到登录页面, 进行登录的操作
// 情况二: 用户已经登录, 那么直接进入到订单页面
router.beforeEach((to, from) => {
// 1.进入到任何别的页面时, 都跳转到login页面
// if (to.path !== "/login") {
// return "/login"
// }
// 2.进入到订单页面时, 判断用户是否登录
const token = localStorage.getItem("token")
if (to.path === "/order" && !token) {
return "/login"
}
})
export default router
Vuex状态管理
什么是状态管理
◼ 在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是 状态管理。
◼ 在前面我们是如何管理自己的状态呢?
在Vue开发中,我们使用组件化的开发方式;
而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state;
在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;
在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions;
Vuex的状态管理
◼ 管理不断变化的state本身是非常困难的:
状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
◼ 因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
在这种模式下,我们的组件树构成了一个巨大的 “试图View”;
不管在树的哪个位置,任何组件都能获取状态或者触发行为;
通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;
◼ 这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想);
◼ 当然,目前Vue官方也在推荐使用Pinia进行状态管理,我们后续也会进行学习。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u97ZrU3z-1689141095340)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618151443794.png)]
Vuex的安装
◼ 依然我们要使用vuex,首先第一步需要安装vuex:
我们这里使用的是vuex4.x;
Vuex的安装
npm install vuex
创建Store
◼ 每一个Vuex应用的核心就是store(仓库):
store本质上是一个容器,它包含着你的应用中大部分的状态(state);
◼ Vuex和单纯的全局对象有什么区别呢?
◼ 第一:Vuex的状态存储是响应式的
当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
◼ 第二:你不能直接改变store中的状态
改变store中的状态的唯一途径就显示提交 (commit) mutation;
这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
◼ 在组件中使用store,我们按照如下的方式:
在模板中使用;
在options api中使用,比如computed;
在setup中使用;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AQ2Ofsp-1689141095341)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230618194127070.png)]
◼ 使用步骤:
创建Store对象;
在app中通过插件安装;
组件获取状态
◼ 在前面我们已经学习过如何在组件中获取状态了。
◼ 当然,如果觉得那种方式有点繁琐(表达式过长),我们可以使用计算属性:
◼ 但是,如果我们有很多个状态都需要获取话,可以使用mapState的辅助函数:
mapState的方式一:对象类型;
mapState的方式二:数组类型;
也可以使用展开运算符和来原有的computed混合在一起;
◼ 注意Store获取到后不能被解构,那么会失去响应式:
<template>
<div class="app">
<button @click="incrementLevel">修改level</button>
<!-- 1.在模板中直接使用多个状态 -->
<h2>name: {
{ $store.state.name }}</h2>
<h2>level: {
{ $store.state.level }}</h2>
<h2>avatar: {
{ $store.state.avatarURL }}</h2>
<!-- 2.计算属性(映射状态: 数组语法) -->
<!-- <h2>name: {
{ name() }}</h2>
<h2>level: {
{ level() }}</h2> -->
<!-- 3.计算属性(映射状态: 对象语法) -->
<!-- <h2>name: {
{ sName }}</h2>
<h2>level: {
{ sLevel }}</h2> -->
<!-- 4.setup计算属性(映射状态: 对象语法) -->
<!-- <h2>name: {
{ cName }}</h2>
<h2>level: {
{ cLevel }}</h2> -->
<!-- 5.setup计算属性(映射状态: 对象语法) -->
<h2>name: {
{ name }}</h2>
<h2>level: {
{ level }}</h2>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
fullname() {
return "xxx"
},
// name() {
// return this.$store.state.name
// },
//返回的是state中对应的字段数据,拿到的是一个一个函数
...mapState(["name", "level", "avatarURL"]),
...mapState({
sName: state => state.name,
sLevel: state => state.level
})
}
}
</script>
<script setup>
import { computed, toRefs } from 'vue'
import { mapState, useStore } from 'vuex'
import useState from "../hooks/useState"
// 1.一步步完成 setup中使用mapState很难用 //返回的是state中对应的字段数据,拿到的是一个一个函数
// const { name, level } = mapState(["name", "level"])
// const store = useStore()
// const cName = computed(name.bind({ $store: store }))
// const cLevel = computed(level.bind({ $store: store }))
// 2.使用useState
// const { name, level } = useState(["name", "level"])
// 3.直接对store.state进行解构(推荐)
const store = useStore()
//用toRefs转成响应式
const { name, level } = toRefs(store.state)
function incrementLevel() {
store.state.level++
}
</script>
<style scoped>
</style>
getters的基本使用
◼ 某些属性我们可能需要经过变化后来使用,这个时候可以使用getters:
getters第二个参数和getters的返回函数
getters: {
// 1.基本使用
doubleCounter(state) {
return state.counter * 2
},
totalAge(state) {
return state.friends.reduce((preValue, item) => {
return preValue + item.age
}, 0)
},
// 2.在该getters属性中, 获取其他的getters
message(state, getters) {
return `name:${
state.name} level:${
state.level} friendTotalAge:${
getters.totalAge}`
},
// 3.getters是可以返回一个函数的, 调用这个函数可以传入参数(了解)
getFriendById(state) {
return function(id) {
const friend = state.friends.find(item => item.id === id)
return friend
}
}
}
mapGetters的辅助函数
<template>
<div class="app">
<button @click="changeAge">修改name</button>
<h2>doubleCounter: {
{ doubleCounter }}</h2>
<h2>friendsTotalAge: {
{ totalAge }}</h2>
<h2>message: {
{ message }}</h2>
<!-- 根据id获取某一个朋友的信息 -->
<h2>id-111的朋友信息: {
{ getFriendById(111) }}</h2>
<h2>id-112的朋友信息: {
{ getFriendById(112) }}</h2>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(["doubleCounter", "totalAge"]),
...mapGetters(["getFriendById"])
}
}
</script>
<script setup>
import { computed, toRefs } from 'vue';
import { mapGetters, useStore } from 'vuex'
const store = useStore()
// 1.使用mapGetters
// const { message: messageFn } = mapGetters(["message"])
// const message = computed(messageFn.bind({ $store: store }))
// 2.直接解构, 并且包裹成ref
// const { message } = toRefs(store.getters)
// 3.针对某一个getters属性使用computed
const message = computed(() => store.getters.message)
function changeAge() {
store.state.name = "kobe"
}
</script>
<style scoped>
</style>
Mutation基本使用
◼ 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
◼ 很多时候我们在提交mutation的时候,会携带一些数据,这个时候我们可以使用参数:
methods: {
changeName() {
// this.$store.state.name = "李银河"
this.$store.commit("changeName", "王小波")
},
incrementLevel() {
this.$store.commit("incrementLevel")
},
changeInfo() {
this.$store.commit(CHANGE_INFO, {
name: "王二",
level: 200
})
}
}
mapMutations辅助函数
◼ 一条重要的原则就是要记住 mutation 必须是同步函数
这是因为devtool工具会记录mutation的日记;
每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
但是在mutation中执行异步操作,就无法追踪到数据的变化;
<template>
<div class="app">
<button @click="changeName('王小波')">修改name</button>
<button @click="incrementLevel">递增level</button>
<button @click="changeInfo({ name: '王二', level: 200 })">修改info</button>
<h2>Store Name: {
{ $store.state.name }}</h2>
<h2>Store Level: {
{ $store.state.level }}</h2>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import { CHANGE_INFO } from "@/store/mutation_types"
export default {
computed: {
},
methods: {
btnClick() {
console.log("btnClick")
},
// ...mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
}
}
</script>
<script setup>
import { mapMutations, useStore } from 'vuex'
import { CHANGE_INFO } from "@/store/mutation_types"
const store = useStore()
// 1.手动的映射和绑定
const mutations = mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
const newMutations = {}
Object.keys(mutations).forEach(key => {
newMutations[key] = mutations[key].bind({ $store: store })
})
const { changeName, incrementLevel, changeInfo } = newMutations
</script>
<style scoped>
</style>
◼ 所以Vuex的重要原则中要求 mutation必须是同步函数;
但是如果我们希望在Vuex中发送网络请求的话需要如何操作呢?就要用到actions了
actions的基本使用
◼ Action类似于mutation,不同在于:
Action提交的是mutation,而不是直接变更状态;
Action可以包含任意异步操作;
◼ 这里有一个非常重要的参数context:
context是一个和store实例均有相同方法和属性的context对象;
所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和
getters;
actions: {
incrementAction(context) {
// console.log(context.commit) // 用于提交mutation
// console.log(context.getters) // getters
// console.log(context.state) // state
context.commit("increment")
},
changeNameAction(context, payload) {
context.commit("changeName", payload)
}
}
◼ 但是为什么它不是store对象呢?这个等到我们讲Modules时再具体来说;
actions的分发操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AlHewIyt-1689141095343)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230619091514779.png)]
actions的辅助函数
◼ action也有对应的辅助函数:
对象类型的写法;
数组类型的写法;
<template>
<div class="home">
<h2>当前计数: {
{ $store.state.counter }}</h2>
<button @click="incrementAction">发起action修改counter</button>
<button @click="increment">递增counter</button>
<h2>name: {
{ $store.state.name }}</h2>
<button @click="changeNameAction('bbbb')">发起action修改name</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
// counterBtnClick() {
// this.$store.dispatch("incrementAction")
// },
// nameBtnClick() {
// this.$store.dispatch("changeNameAction", "aaa")
// }
// ...mapActions(["incrementAction", "changeNameAction"])
}
}
</script>
<script setup>
import { useStore, mapActions } from 'vuex'
const store = useStore()
// 1.在setup中使用mapActions辅助函数
// const actions = mapActions(["incrementAction", "changeNameAction"])
// const newActions = {}
// Object.keys(actions).forEach(key => {
// newActions[key] = actions[key].bind({ $store: store })
// })
// const { incrementAction, changeNameAction } = newActions
// 2.使用默认的做法
function increment() {
store.dispatch("incrementAction")
}
</script>
<style scoped>
</style>
actions的异步操作
◼ Action 通常是异步的,那么如何知道 action 什么时候结束呢?
我们可以通过让action返回Promise,在Promise的then中来处理完成后的操作;
fetchHomeMultidataAction(context) {
// 1.返回Promise, 给Promise设置then
// fetch("http://123.207.32.32:8000/home/multidata").then(res => {
// res.json().then(data => {
// console.log(data)
// })
// })
// 2.Promise链式调用
// fetch("http://123.207.32.32:8000/home/multidata").then(res => {
// return res.json()
// }).then(data => {
// console.log(data)
// })
return new Promise(async (resolve, reject) => {
// 3.await/async
const res = await fetch("http://123.207.32.32:8000/home/multidata")
const data = await res.json()
// 修改state数据
context.commit("changeBanners", data.data.banner.list)
context.commit("changeRecommends", data.data.recommend.list)
resolve("aaaaa")
})
}
<template>
<div class="home">
<h2>Home Page</h2>
<ul>
<template v-for="item in $store.state.banners" :key="item.acm">
<li>{
{ item.title }}</li>
</template>
</ul>
</div>
</template>
<script>
</script>
<script setup>
import { useStore } from 'vuex'
// 告诉Vuex发起网络请求
const store = useStore()
store.dispatch("fetchHomeMultidataAction").then(res => {
console.log("home中的then被回调:", res)
})
</script>
module的基本使用
◼ 什么是Module?
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿;
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块;
counter.js
const counter = {
namespaced: true,
state: () => ({
count: 99
}),
mutations: {
incrementCount(state) {
console.log(state)
state.count++
}
},
getters: {
doubleCount(state, getters, rootState) {
return state.count + rootState.rootCounter
}
},
actions: {
incrementCountAction(context) {
context.commit("incrementCount")
}
}
}
export default counter
index.js
import counterModule from './modules/counter'
modules: {
counter: counterModule
}
组件中:
<template>
<div class="home">
<h2>Home Page</h2>
<ul>
<!-- 获取数据: 需要从模块中获取 state.modulename.xxx -->
<template v-for="item in $store.state.home.banners" :key="item.acm">
<li>{
{ item.title }}</li>
</template>
</ul>
</div>
</template>
<script>
</script>
<script setup>
import { useStore } from 'vuex'
// 告诉Vuex发起网络请求
const store = useStore()
store.dispatch("fetchHomeMultidataAction").then(res => {
console.log("home中的then被回调:", res)
})
</script>
<style scoped>
</style>
module的局部状态
◼ 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象:
home.js
export default {
state: () => ({
// 服务器数据
banners: [],
recommends: []
}),
mutations: {
changeBanners(state, banners) {
state.banners = banners
},
changeRecommends(state, recommends) {
state.recommends = recommends
}
},
actions: {
fetchHomeMultidataAction(context) {
return new Promise(async (resolve, reject) => {
// 3.await/async
const res = await fetch("http://123.207.32.32:8000/home/multidata")
const data = await res.json()
// 修改state数据
context.commit("changeBanners", data.data.banner.list)
context.commit("changeRecommends", data.data.recommend.list)
resolve("aaaaa")
})
}
}
}
home.vue
<template>
<div class="home">
<h2>Home Page</h2>
<!-- 1.使用state时, 是需要state.moduleName.xxx -->
<h2>Counter模块的counter: {
{ $store.state.counter.count }}</h2>
<!-- 2.使用getters时, 是直接getters.xxx -->
<h2>Counter模块的doubleCounter: {
{ $store.getters.doubleCount }}</h2>
<button @click="incrementCount">count模块+1</button>
</div>
</template>
<script>
</script>
<script setup>
import { useStore } from 'vuex'
// 告诉Vuex发起网络请求
const store = useStore()
// 派发事件时, 默认也是不需要跟模块名称
// 提交mutation时, 默认也是不需要跟模块名称
function incrementCount() {
store.dispatch("incrementCountAction")
}
</script>
<style scoped>
</style>
module的命名空间
◼ 默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的:
这样使得多个模块能够对同一个 action 或 mutation 作出响应;
Getter 同样也默认注册在全局命名空间;
◼ 如果我们希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:
当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bPMtcR5k-1689141095345)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230620153102896.png)]
<template>
<div class="home">
<h2>Home Page</h2>
<!-- 1.使用state时, 是需要state.moduleName.xxx -->
<h2>Counter模块的counter: {
{ $store.state.counter.count }}</h2>
<!-- 2.使用getters时, 是直接getters.xxx -->
<h2>Counter模块的doubleCounter: {
{ $store.getters["counter/doubleCount"] }}</h2>
<button @click="incrementCount">count模块+1</button>
</div>
</template>
<script>
</script>
<script setup>
import { useStore } from 'vuex'
// 告诉Vuex发起网络请求
const store = useStore()
// 派发事件时, 默认也是不需要跟模块名称
// 提交mutation时, 默认也是不需要跟模块名称
function incrementCount() {
store.dispatch("counter/incrementCountAction")
}
</script>
<style scoped>
</style>
module修改或派发根组件
◼ 如果我们希望在action中修改root中的state,那么有如下的方式:
Pinia状态管理
什么是Pinia呢?
◼ Pinia(发音为/piːnjʌ/,如英语中的“peenya”)是最接近piña(西班牙语中的菠萝)的词;
Pinia开始于大概2019年,最初是作为一个实验为Vue重新设计状态管理,让它用起来像组合式API(Composition API)。
从那时到现在,最初的设计原则依然是相同的,并且目前同时兼容Vue2、Vue3,也并不要求你使用Composition API;
Pinia本质上依然是一个状态管理的库,用于跨组件、页面进行状态共享(这点和Vuex、Redux一样);
Pinia和Vuex的区别
◼ 那么我们不是已经有Vuex了吗?为什么还要用Pinia呢?
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法;
最终,团队意识到Pinia已经实现了Vuex5中大部分内容,所以最终决定用Pinia来替代Vuex;
与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的仪式,提供了 Composition-API 风格的 API;
最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持;
◼ 和Vuex相比,Pinia有很多的优势:
比如mutations 不再存在:
✓ 他们经常被认为是 非常 冗长;
✓ 他们最初带来了 devtools 集成,但这不再是问题;
更友好的TypeScript支持,Vuex之前对TS的支持很不友好;
不再有modules的嵌套结构:
✓ 你可以灵活使用每一个store,它们是通过扁平化的方式来相互使用的;
也不再有命名空间的概念,不需要记住它们的复杂关系;
如何使用Pinia?
认识Store
◼ 什么是Store?
一个 Store (如 Pinia)是一个实体,它会持有为绑定到你组件树的状态和业务逻辑,也就是保存了全局的状态;
它有点像始终存在,并且每个人都可以读取和写入的组件;
你可以在你的应用程序中定义任意数量的Store来管理你的状态;
◼ Store有三个核心概念:
state、getters、actions;
等同于组件的data、computed、methods;
一旦 store 被实例化,你就可以直接在 store 上访问 state、getters 和 actions 中定义的任何属性;
index.js
import {
createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
counter.js
// 定义关于counter的store
import {
defineStore } from 'pinia'
import useUser from './user'
const useCounter = defineStore("counter", {
state: () => ({
count: 99,
friends: [
{
id: 111, name: "why" },
{
id: 112, name: "kobe" },
{
id: 113, name: "james" },
]
}),
getters: {
// 1.基本使用
doubleCount(state) {
return state.count * 2
},
// 2.一个getter引入另外一个getter
doubleCountAddOne() {
// this是store实例
return this.doubleCount + 1
},
// 3.getters也支持返回一个函数
getFriendById(state) {
return function(id) {
for (let i = 0; i < state.friends.length; i++) {
const friend = state.friends[i]
if (friend.id === id) {
return friend
}
}
}
},
// 4.getters中用到别的store中的数据
showMessage(state) {
// 1.获取user信息
const userStore = useUser()
// 2.获取自己的信息
// 3.拼接信息
return `name:${
userStore.name}-count:${
state.count}`
}
},
actions: {
increment() {
this.count++
},
incrementNum(num) {
this.count += num
}
}
})
export default useCounter
组件中:
<template>
<div class="home">
<h2>Home View</h2>
<h2>count: {
{ counterStore.count }}</h2>
<h2>count: {
{ count }}</h2>
<button @click="incrementCount">count+1</button>
</div>
</template>
<script setup>
import { toRefs } from 'vue'
import { storeToRefs } from 'pinia'
import useCounter from '@/stores/counter';
const counterStore = useCounter()
// const { count } = toRefs(counterStore)
const { count } = storeToRefs(counterStore)
function incrementCount() {
counterStore.count++
}
</script>
<style scoped>
</style>
定义一个Store
◼ 定义一个Store:
我们需要知道 Store 是使用 defineStore() 定义的,
并且它需要一个唯一名称,作为第一个参数传递;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7fW49w4-1689141095346)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230620225310733.png)]
◼ 这个name,也称为id,是必要的,Pinia 使用它来将 store 连接到 devtools。
◼ 返回的函数统一使用useX作为命名方案,这是约定的规范;
使用定义的Store
◼ Store在它被使用之前是不会创建的,我们可以通过调用use函数来使用Store:
◼ 注意Store获取到后不能被解构,那么会失去响应式:
为了从 Store 中提取属性同时保持其响应式,您需要使用storeToRefs()。
认识和定义State
◼ state 是 store 的核心部分,因为store是用来帮助我们管理状态的。
在 Pinia 中,状态被定义为返回初始状态的函数;
操作State(一)
◼ 读取和写入 state:
默认情况下,您可以通过 store 实例访问状态来直接读取和写入状态;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rrGnhcSr-1689141095347)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230620230737197.png)]
◼ 重置 State:
你可以通过调用 store 上的 $reset() 方法将状态 重置 到其初始值;
操作State(二)
◼ 改变State:
除了直接用 store.counter++ 修改 store,你还可以调用 $patch 方法;
它允许您使用部分“state”对象同时应用多个更改;
◼ 替换State:
您可以通过将其 $state 属性设置为新对象来替换 Store 的整个状态:
// 3.替换state为新的对象
const oldState = userStore.$state
userStore.$state = {
name: "curry",
level: 200
}
console.log(oldState === userStore.$state)//true
认识和定义Getters
◼ Getters相当于Store的计算属性:
它们可以用 defineStore() 中的 getters 属性定义;
getters中可以定义接受一个state作为参数的函数;
index.js
//getters中用到别的store中的数据
import useUser from './user'
getters: {
// 1.基本使用
doubleCount(state) {
return state.count * 2
},
// 2.一个getter引入另外一个getter
doubleCountAddOne() {
// this是store实例
return this.doubleCount + 1
},
// 3.getters也支持返回一个函数
getFriendById(state) {
return function(id) {
for (let i = 0; i < state.friends.length; i++) {
const friend = state.friends[i]
if (friend.id === id) {
return friend
}
}
}
},
// 4.getters中用到别的store中的数据
showMessage(state) {
// 1.获取user信息
const userStore = useUser()
// 2.获取自己的信息
// 3.拼接信息
return `name:${
userStore.name}-count:${
state.count}`
}
},
组件中:
<template>
<div class="home">
<h2>Home View</h2>
<h2>doubleCount: {
{ counterStore.doubleCount }}</h2>
<h2>doubleCountAddOne: {
{ counterStore.doubleCountAddOne }}</h2>
<h2>friend-111: {
{ counterStore.getFriendById(111) }}</h2>
<h2>friend-112: {
{ counterStore.getFriendById(112) }}</h2>
<h2>showMessage: {
{ counterStore.showMessage }}</h2>
<button @click="changeState">修改state</button>
<button @click="resetState">重置state</button>
</div>
</template>
<script setup>
import useCounter from '@/stores/counter';
const counterStore = useCounter()
</script>
<style scoped>
</style>
认识和定义Actions
◼ Actions 相当于组件中的 methods。
可以使用 defineStore() 中的 actions 属性定义,并且它们非常适合定义业务逻辑;
◼ 和getters一样,在action中可以通过this访问整个store实例的所有操作;
index.js
actions: {
increment() {
this.count++
},
incrementNum(num) {
this.count += num
}
}
组件中:
<template>
<div class="home">
<h2>Home View</h2>
<h2>doubleCount: {
{ counterStore.count }}</h2>
<button @click="changeState">修改state</button>
<!-- 展示数据 -->
<h2>轮播的数据</h2>
<ul>
<template v-for="item in homeStore.banners">
<li>{
{ item.title }}</li>
</template>
</ul>
</div>
</template>
<script setup>
import useCounter from '@/stores/counter';
import useHome from '@/stores/home';
const counterStore = useCounter()
function changeState() {
// counterStore.increment()
counterStore.incrementNum(10)
}
const homeStore = useHome()
homeStore.fetchHomeMultidata().then(res => {
console.log("fetchHomeMultidata的action已经完成了:", res)
})
</script>
<style scoped>
</style>
Actions执行异步操作
◼ 并且Actions中是支持异步操作的,并且我们可以编写异步函数,在函数中使用await;
actions: {
async fetchHomeMultidata() {
const res = await fetch("http://123.207.32.32:8000/home/multidata")
const data = await res.json()
this.banners = data.data.banner.list
this.recommends = data.data.recommend.list
// return new Promise(async (resolve, reject) => {
// const res = await fetch("http://123.207.32.32:8000/home/multidata")
// const data = await res.json()
// this.banners = data.data.banner.list
// this.recommends = data.data.recommend.list
// resolve("bbb")
// })
}
}
组件中:
<template>
<div class="home">
<h2>Home View</h2>
<h2>doubleCount: {
{ counterStore.count }}</h2>
<button @click="changeState">修改state</button>
<!-- 展示数据 -->
<h2>轮播的数据</h2>
<ul>
<template v-for="item in homeStore.banners">
<li>{
{ item.title }}</li>
</template>
</ul>
</div>
</template>
<script setup>
import useCounter from '@/stores/counter';
import useHome from '@/stores/home';
const counterStore = useCounter()
function changeState() {
// counterStore.increment()
counterStore.incrementNum(10)
}
const homeStore = useHome()
homeStore.fetchHomeMultidata().then(res => {
console.log("fetchHomeMultidata的action已经完成了:", res)
})
</script>
<style scoped>
</style>
网络请求库 – axios库
axios请求方式
◼ 支持多种请求方式:
axios(config)
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
◼ 有时候, 我们可能需求同时发送两个请求
使用axios.all, 可以放入多个请求的数组.
axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2
常见的配置选项
◼ 请求地址
url: ‘/user’,
◼ 请求类型
method: ‘get’,
◼ 请根路径
baseURL: ‘http://www.mt.com/api’,
◼ 请求前的数据处理
transformRequest:[function(data){}],
◼ 请求后的数据处理
transformResponse: [function(data){}],
◼ 自定义的请求头
headers:{‘x-Requested-With’:‘XMLHttpRequest’},
◼ URL查询对象
params:{ id: 12 },
◼ 查询对象序列化函数
paramsSerializer: function(params){ }
◼ request body
data: { key: ‘aa’},
◼ 超时设置
timeout: 1000,
axios的创建实例
◼ 为什么要创建axios的实例呢?
当我们从axios模块中导入对象时, 使用的实例是默认的实例;
当给该实例设置一些默认配置时, 这些配置就被固定下来了.
但是后续开发中, 某些配置可能会不太一样;
比如某些请求需要使用特定的baseURL或者timeout等.
这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUU6loMw-1689141095349)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230621110753857.png)]
// axios默认库提供给我们的实例对象
axios.get("http://123.207.32.32:9001/lyric?id=500665346")
// 创建其他的实例发送网络请求
const instance1 = axios.create({
baseURL: "http://123.207.32.32:9001",
timeout: 6000,
headers: {
}
})
instance1.get("/lyric", {
params: {
id: 500665346
}
}).then(res => {
console.log("res:", res.data)
})
const instance2 = axios.create({
baseURL: "http://123.207.32.32:8000",
timeout: 10000,
headers: {
}
})
常见请求演练.js
// 1.发送request请求
// axios.request({
// url: "http://123.207.32.32:8000/home/multidata",
// method: "get"
// }).then(res => {
// console.log("res:", res.data)
// })
// 2.发送get请求
// axios.get(`http://123.207.32.32:9001/lyric?id=500665346`).then(res => {
// console.log("res:", res.data.lrc)
// })
// axios.get("http://123.207.32.32:9001/lyric", {
// params: {
// id: 500665346
// }
// }).then(res => {
// console.log("res:", res.data.lrc)
// })
// 3.发送post请求
// axios.post("http://123.207.32.32:1888/02_param/postjson", {
// name: "coderwhy",
// password: 123456
// }).then(res => {
// console.log("res", res.data)
// })
axios.post("http://123.207.32.32:1888/02_param/postjson", {
data: {
name: "coderwhy",
password: 123456
}
}).then(res => {
console.log("res", res.data)
})
额外知识补充.js
import axios from 'axios'
// 1.baseURL
const baseURL = "http://123.207.32.32:8000"
// 给axios实例配置公共的基础配置
axios.defaults.baseURL = baseURL
axios.defaults.timeout = 10000
axios.defaults.headers = {
}
// 1.1.get: /home/multidata
axios.get("/home/multidata").then(res => {
console.log("res:", res.data)
})
// 1.2.get: /home/data
// 2.axios发送多个请求
// Promise.all
axios.all([
axios.get("/home/multidata"),
axios.get("http://123.207.32.32:9001/lyric?id=500665346")
]).then(res => {
console.log("res:", res)
})
请求和响应拦截器
◼ axios的也可以设置拦截器:拦截每次请求和响应
axios.interceptors.request.use(请求成功拦截, 请求失败拦截)
axios.interceptors.response.use(响应成功拦截, 响应失败拦截)
import axios from 'axios'
// 对实例配置拦截器
axios.interceptors.request.use((config) => {
console.log("请求成功的拦截")
// 1.开始loading的动画
// 2.对原来的配置进行一些修改
// 2.1. header
// 2.2. 认证登录: token/cookie
// 2.3. 请求参数进行某些转化
return config
}, (err) => {
console.log("请求失败的拦截")
return err
})
axios.interceptors.response.use((res) => {
console.log("响应成功的拦截")
// 1.结束loading的动画
// 2.对数据进行转化, 再返回数据
return res.data
}, (err) => {
console.log("响应失败的拦截:", err)
return err
})
axios.get("http://123.207.32.32:9001/lyric?id=500665346").then(res => {
console.log("res:", res)
}).catch(err => {
console.log("err:", err)
})
封装自己的好维护的axios
index.js
import axios from 'axios'
class HYRequest {
constructor(baseURL, timeout=10000) {
this.instance = axios.create({
baseURL,
timeout
})
}
request(config) {
return new Promise((resolve, reject) => {
this.instance.request(config).then(res => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
get(config) {
return this.request({
...config, method: "get" })
}
post(config) {
return this.request({
...config, method: "post" })
}
}
export default new HYRequest("http://123.207.32.32:9001")
组件中:
import {
createApp } from 'vue'
import axios from 'axios'
import App from './App.vue'
import hyRequest from './service'
createApp(App).mount('#app')
hyRequest.request({
url: "/lyric?id=500665346"
}).then(res => {
console.log("res:", res)
})
hyRequest.get({
url: "/lyric",
params: {
id: 500665346
}
}).then(res => {
console.log("res:", res)
})
Vue3 – 高级语法补充
认识自定义指令
◼ 在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来
自定义自己的指令。
注意:在Vue中,代码的复用和抽象主要还是通过组件;
通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;
◼ 自定义指令分为两种:
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
◼ 比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
实现方式一:如果我们使用默认的实现方式;
实现方式二:自定义一个 v-focus 的局部指令;
实现方式三:自定义一个 v-focus 的全局指令;
实现方式一:聚焦的默认实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZ8o2ccQ-1689141095350)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230621151636469.png)]
实现方式二:局部自定义指令
◼ 实现方式二:自定义一个 v-focus 的局部指令
这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);
自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;
<template>
<div class="app">
<!-- <input type="text" ref="inputRef"> -->
<input type="text" v-focus>
</div>
</template>
<!-- <script>
export default {
directives: {
focus: {
// 生命周期的函数(自定义指令)
mounted(el) {
// console.log("v-focus应用的元素被挂载了", el)
el?.focus()
}
}
}
}
</script> -->
<script setup>
// 1.方式一: 定义ref绑定到input中, 调用focus
// import useInput from "./hooks/useInput"
// const { inputRef } = useInput()
// 2.方式二: 自定义指令(局部指令)
// const vFocus = {
// // 生命周期的函数(自定义指令)
// mounted(el) {
// // console.log("v-focus应用的元素被挂载了", el)
// el?.focus()
// }
// }
</script>
<style scoped>
</style>
实现方式三:自定义全局指令
focus.js
export default function directiveFocus(app) {
app.directive("focus", {
// 生命周期的函数(自定义指令)
mounted(el) {
// console.log("v-focus应用的元素被挂载了", el)
el?.focus()
}
})
}
index.js
import directiveFocus from "./focus"
import directiveUnit from "./unit"
import directiveFtime from "./ftime"
// export default function useDirectives(app) {
// directiveFocus(app)
// directiveUnit(app)
// directiveFtime(app)
// }
export default function directives(app) {
directiveFocus(app)
directiveUnit(app)
directiveFtime(app)
}
main.js
import {
createApp } from 'vue'
// import App from './01_自定义指令/App.vue'
// import App from './02_内置组件补充/App.vue'
// import App from './03_安装插件/App.vue'
// import App from './04_Render函数/App.vue'
// import App from './05_JSX的语法/App.vue'
import App from './06_过渡动画/App.vue'
// import useDirectives from "./01_自定义指令/directives/index"
import directives from "./01_自定义指令/directives/index"
// import router from "./router"
// 自定义指令的方式一:
// const app = createApp(App)
// // useDirectives(app)
// directives(app)
// app.mount('#app')
// 自定义指令的方式二:使用插件
createApp(App).use(directives).mount("#app")
指令的生命周期
◼ 一个指令定义的对象,Vue提供了如下的几个钩子函数:
◼ created:在绑定元素的 attribute 或事件监听器被应用之前调用;
◼ beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
◼ mounted:在绑定元素的父组件被挂载后调用;
◼ beforeUpdate:在更新包含组件的 VNode 之前调用;
◼ updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
◼ beforeUnmount:在卸载绑定元素的父组件之前调用;
◼ unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
<template>
<div class="app">
<button @click="counter++">+1</button>
<button @click="showTitle = false">隐藏</button>
<h2 v-if="showTitle" class="title" v-why>当前计数: {
{ counter }}</h2>
</div>
</template>
<script setup>
import { ref } from 'vue';
const counter = ref(0)
const showTitle = ref(true)
const vWhy = {
created() {
console.log("created")
},
beforeMount() {
console.log("beforeMount")
},
mounted() {
console.log("mounted")
},
beforeUpdate() {
console.log("beforeUpdate")
},
updated() {
console.log("updated")
},
beforeUnmount() {
console.log("beforeUnmount")
},
unmounted() {
console.log("unmounted")
}
}
</script>
<style scoped>
</style>
指令的参数和修饰符
<template>
<div class="app">
<button @click="counter++">+1</button>
<!-- 1.参数-修饰符-值 -->
<!-- <h2 v-why:kobe.abc.cba="message">哈哈哈哈</h2> -->
<!-- 2.价格拼接单位符号 -->
<h2 v-unit> {
{ 111 }} </h2>
</div>
</template>
<script setup>
import { ref } from 'vue';
const counter = ref(0)
const message = '你好啊, 李银河'
const vWhy = {
mounted(el, bindings) {
console.log(bindings)
el.textContent = bindings.value
}
}
</script>
<style scoped>
</style>
unit.js
export default function directiveUnit(app) {
app.directive("unit", {
mounted(el, bindings) {
const defaultText = el.textContent
let unit = bindings.value
if (!unit) {
unit = "¥"
}
el.textContent = unit + defaultText
}
})
}
时间格式化指令
ftime.js
import dayjs from 'dayjs'
export default function directiveFtime(app) {
app.directive("ftime", {
mounted(el, bindings) {
// 1.获取时间, 并且转化成毫秒
let timestamp = el.textContent
if (timestamp.length === 10) {
timestamp = timestamp * 1000
}
timestamp = Number(timestamp)
// 2.获取传入的参数
let value = bindings.value
if (!value) {
value = "YYYY-MM-DD HH:mm:ss"
}
// 3.对时间进行格式化
const formatTime = dayjs(timestamp).format(value)
el.textContent = formatTime
}
})
}
组件中:
<template>
<div class="app">
<h2 v-ftime="'YYYY/MM/DD'">{
{ timestamp }}</h2>
<h2 v-ftime>{
{ 1551111166666 }}</h2>
</div>
</template>
<script setup>
const timestamp = 1231355453
</script>
<style scoped>
</style>
认识Teleport
◼ 在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
那么组件A中template的元素,会被挂载到组件B中template的某个位置;
最终我们的应用程序会形成一颗DOM树结构;
◼ 但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
比如移动到body元素上,或者我们有其他的div#app之外的元素上;
这个时候我们就可以通过teleport来完成;
◼ Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals;
teleport翻译过来是心灵传输、远距离运输的意思;
✓ 它有两个属性:
➢ to:指定将其中的内容移动到的目标元素,可以使用选择器;
➢ disabled:是否禁用 teleport 的功能;
多个teleport
◼ 如果我们将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并
而不是:◼ 实现效果如下:
异步组件和Suspense
◼ 注意:目前(2022-08-01)Suspense显示的是一个实验性的特性,API随时可能会修改。
◼ Suspense是一个内置的全局组件,该组件有两个插槽:
default:如果default可以显示,那么显示default的内容;
fallback:如果default无法显示,那么会显示fallback插槽的内容;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aSwYiZqd-1689141095352)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230622121623905.png)]
<div class="app">
<suspense>
<template #default>
<async-home/>
</template>
<template #fallback>//应急组件,在上面组件没下载下来之前,现在这个组件,用于loading
<h2>Loading</h2>
</template>
</suspense>
</div>
认识Vue插件
◼ 通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
函数类型:一个function,这个函数会在安装插件时自动执行;
◼ 插件可以完成的功能没有限制,比如下面的几种都是可以的:
添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
添加全局资源:指令/过滤器/过渡等;
通过全局 mixin 来添加一些组件选项;
一个库,提供自己的 API,同时提供上面提到的一个或多个功能;
// 安装插件
// 方式一: 传入对象的情况
app.use({
install: function(app) {
console.log("传入对象的install被执行:", app)
}
})
// 方式二: 传入函数的情况
app.use(function(app) {
console.log("传入函数被执行:", app)
})
main.js
import {
createApp } from 'vue'
// import App from './01_自定义指令/App.vue'
// import App from './02_内置组件补充/App.vue'
// import App from './03_安装插件/App.vue'
// import App from './04_Render函数/App.vue'
// import App from './05_JSX的语法/App.vue'
import App from './06_过渡动画/App.vue'
// import useDirectives from "./01_自定义指令/directives/index"
import directives from "./01_自定义指令/directives/index"
// import router from "./router"
// 自定义指令的方式一:
// const app = createApp(App)
// // useDirectives(app)
// directives(app)
// app.mount('#app')
// 自定义指令的方式二:使用插件
createApp(App).use(directives).mount("#app")
认识h函数
◼ Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时
候你可以使用 渲染函数 ,它比模板更接近编译器;
◼ 前面我们讲解过VNode和VDOM的概念:
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM
(VDOM);
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
◼ 那么我们应该怎么来做呢?使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;
h()函数 如何使用呢?
◼ h()函数 如何使用呢?它接受三个参数:
h函数的基本使用
◼ h函数可以在两个地方使用:
render函数选项中;
setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
<script>
import { h } from 'vue'
export default {
render() {
return h("div", { className: "app" }, [
h("h2", { className: "title" }, "我是标题"),
h("p", { className: "content" }, "我是内容, 哈哈哈"),
])
}
}
</script>
<style scoped>
</style>
h函数计数器案例
option组件用法:
<script>
import { h } from 'vue'
import Home from "./Home.vue"
export default {
data() {
return {
counter: 0
}
},
render() {
return h("div", { className: "app" }, [
h("h2", null, `当前计数: ${this.counter}`),
h("button", { onClick: this.increment }, "+1"),
h("button", { onClick: this.decrement }, "-1"),
h(Home)
])
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
}
</script>
<style scoped>
</style>
composition组件用法:
<template>//setup语法糖要这么写
<render/>
<h2 class="">内容</h2>
</template>
<!-- <script>
import { h, ref } from 'vue'
import Home from "./Home.vue"
export default {
setup() {
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
return () => h("div", { className: "app" }, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", { onClick: increment }, "+1"),
h("button", { onClick: decrement }, "-1"),
h(Home)
])
}
}
</script> -->
<script setup>
import { ref, h } from 'vue';
import Home from './Home.vue'
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
const render = () => h("div", { className: "app" }, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", { onClick: increment }, "+1"),
h("button", { onClick: decrement }, "-1"),
h(Home)
])
</script>
<style scoped>
</style>
jsx的babel配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YC5Po0b-1689141095352)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230622155459109.png)]
<script lang="jsx">
export default {
render() {
return (
<div class="app">
<h2>我是标题</h2>
<p>我是内容, 哈哈哈</p>
</div>
)
}
}
</script>
<style lang="less" scoped>
</style>
jsx计数器案例
<script lang="jsx">
import About from './About.vue'
export default {
data() {
return {
counter: 0
}
},
render() {
return (
<div class="app">
<h2>当前计数: { this.counter }</h2>
<button onClick={ this.increment }>+1</button>
<button onClick={ this.decrement }>-1</button>
<About/>
</div>
)
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
}
</script>
<style lang="less" scoped>
</style>
composition使用方法
<template>
<jsx/>
</template>
<!-- <script lang="jsx">
import { ref } from 'vue'
import About from './About.vue'
export default {
setup() {
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
return () => (
<div class="app">
<h2>当前计数: { counter.value }</h2>
<button onClick={ increment }>+1</button>
<button onClick={ decrement }>-1</button>
<About/>
</div>
)
}
}
</script> -->
<script lang="jsx" setup>
import { ref } from 'vue'
import About from "./About.vue"
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
const jsx = () => (
<div class="app">
<h2>当前计数: { counter.value }</h2>
<button onClick={ increment }>+1</button>
<button onClick={ decrement }>-1</button>
<About/>
</div>
)
</script>
<style lang="less" scoped>
</style>
Vue3 – 实现过渡动画
◼ 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验:
React框架本身并没有提供任何动画相关的API,所以在React中使用过渡动画我们需要使用一个第三方库 react-transition-group;
Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果;
◼ 我们来看一个案例:
Hello World的显示和隐藏;
通过下面的代码实现,是不会有任何动画效果的;
◼ 没有动画的情况下,整个内容的显示和隐藏会非常的生硬:
如果我们希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画;
Vue的transition动画
Transition组件的原理
◼ 我们会发现,Vue自动给h2元素添加了动画,这是什么原因呢?
◼ 当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
1.自动嗅探目标元素是否应用了CSS过渡或者动画,如果有,那么在恰当的时机添加/删除 CSS类名;
2.如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用;
3.如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行;
◼ 那么都会添加或者删除哪些class呢?
过渡动画class
◼ 我们会发现上面提到了很多个class,事实上Vue就是帮助我们在这些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 被删除),在过渡/动画完成之后移除。
class添加的时机和命名规则
过渡css动画
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<transition name="why">
<h2 v-if="isShow">
要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏。不能说,也不能想,却又不能忘。它们不能变成语言,它们无法变成语言,一旦变成语言就不再是它们了。它们是一片朦胧的温馨与寂寥,是一片成熟的希望与绝望,它们的领地只有两处:心与坟墓。比如说邮票,有些是用于寄信的,有些仅仅是为了收藏。
</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(false)
</script>
<style scoped>
h2 {
display: inline-block;
}
.why-enter-active {
animation: whyAnim 2s ease;
}
.why-leave-active {
/* animation: whyLeaveAnim 2s ease; */
animation: whyAnim 2s ease reverse;
}
@keyframes whyAnim {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes whyLeaveAnim {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(-500px);
opacity: 0;
}
}
</style>
同时设置过渡和动画(一般不设置)
◼ Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:
如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听;
◼ 但是如果我们同时使用了过渡和动画呢?
并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束;
在这种情况下,我们可以设置 type 属性为 animation 或者 transition 来明确的告知Vue监听的类型;
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<transition name="why">
<h2 v-if="isShow">
要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏。不能说,也不能想,却又不能忘。它们不能变成语言,它们无法变成语言,一旦变成语言就不再是它们了。它们是一片朦胧的温馨与寂寥,是一片成熟的希望与绝望,它们的领地只有两处:心与坟墓。比如说邮票,有些是用于寄信的,有些仅仅是为了收藏。
</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(false)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
显示指定动画时间
◼ 我们也可以显示指定过渡的时间,通过 duration 属性。
◼ duration可以设置两种类型的值:
number类型:同时设置进入和离开的过渡时间;
object类型:分别设置进入和离开的过渡时间;
过渡的模式mode
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<!-- mode属性掌握 -->
<transition name="why" mode="out-in">
<h2 v-if="isShow">哈哈哈</h2>
<h2 v-else>呵呵呵</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(true)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
动态组件的切换
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<!-- mode属性掌握 -->
<transition name="why" mode="out-in" appear="">
<component :is=" isShow ? 'home': 'about'"></component>
</transition>
</div>
</template>
<script>
import Home from './pages/Home.vue'
import About from './pages/About.vue'
export default {
components: {
Home,
About
}
}
</script>
<script setup>
import { ref } from 'vue';
const isShow = ref(true)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
appear初次渲染
认识列表的过渡
◼ 目前为止,过渡动画我们只要是针对单个元素或者组件的:
要么是单个节点;
要么是同一时间渲染多个节点中的一个;
◼ 那么如果希望渲染的是一个列表,并且该列表中添加删除数据也希望有动画执行呢?
这个时候我们要使用 组件来完成;
◼ 使用 有如下的特点:
默认情况下,它不会渲染一个元素的包裹器,但是你可以指定一个元素并以 tag attribute 进行渲染;
过渡模式不可用,因为我们不再相互切换特有的元素;
内部元素总是需要提供唯一的 key attribute 值;
CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身;
<template>
<div class="app">
<button @click="addNumber">添加数字</button>
<button @click="removeNumber">删除数字</button>
<button @click="shuffleNumber">打乱数字</button>
<transition-group tag="div" name="why">
<template v-for="item in nums" :key="item">
<span>{
{ item }}</span>
</template>
</transition-group>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { shuffle } from "underscore";
const nums = ref([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
const addNumber = () => {
nums.value.splice(randomIndex(), 0, nums.value.length)
}
const removeNumber = () => {
nums.value.splice(randomIndex(), 1)
}
const shuffleNumber = () => {
nums.value = shuffle(nums.value)
}
const randomIndex = () => {
return Math.floor(Math.random() * nums.value.length)
}
</script>
<style scoped>
span {
margin-right: 10px;
display: inline-block;
}
.why-enter-from,
.why-leave-to {
opacity: 0;
transform: translateY(30px);
}
.why-enter-to,
.why-leave-from {
opacity: 1;
transform: translateY(0);
}
.why-enter-active,
.why-leave-active {
transition: all 2s ease;
}
.why-leave-active {
position: absolute;
}
/* 针对其他移动的阶段需要的动画 */
.why-move {
transition: all 2s ease;
}
</style>
default {
data() {
return {
counter: 0
}
},
render() {
return (
<div class="app">
<h2>当前计数: { this.counter }</h2>
<button onClick={ this.increment }>+1</button>
<button onClick={ this.decrement }>-1</button>
<About/>
</div>
)
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
}
##### composition使用方法
```vue
<template>
<jsx/>
</template>
<!-- <script lang="jsx">
import { ref } from 'vue'
import About from './About.vue'
export default {
setup() {
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
return () => (
<div class="app">
<h2>当前计数: { counter.value }</h2>
<button onClick={ increment }>+1</button>
<button onClick={ decrement }>-1</button>
<About/>
</div>
)
}
}
</script> -->
<script lang="jsx" setup>
import { ref } from 'vue'
import About from "./About.vue"
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
const jsx = () => (
<div class="app">
<h2>当前计数: { counter.value }</h2>
<button onClick={ increment }>+1</button>
<button onClick={ decrement }>-1</button>
<About/>
</div>
)
</script>
<style lang="less" scoped>
</style>
Vue3 – 实现过渡动画
◼ 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验:
React框架本身并没有提供任何动画相关的API,所以在React中使用过渡动画我们需要使用一个第三方库 react-transition-group;
Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果;
◼ 我们来看一个案例:
Hello World的显示和隐藏;
通过下面的代码实现,是不会有任何动画效果的;
[外链图片转存中…(img-RzZlm78x-1689141095353)]
◼ 没有动画的情况下,整个内容的显示和隐藏会非常的生硬:
如果我们希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画;
Vue的transition动画
[外链图片转存中…(img-2KvaJvlt-1689141095353)]
Transition组件的原理
◼ 我们会发现,Vue自动给h2元素添加了动画,这是什么原因呢?
◼ 当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
1.自动嗅探目标元素是否应用了CSS过渡或者动画,如果有,那么在恰当的时机添加/删除 CSS类名;
2.如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用;
3.如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行;
◼ 那么都会添加或者删除哪些class呢?
过渡动画class
◼ 我们会发现上面提到了很多个class,事实上Vue就是帮助我们在这些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 被删除),在过渡/动画完成之后移除。
class添加的时机和命名规则
[外链图片转存中…(img-3aeWz3hV-1689141095353)]
过渡css动画
[外链图片转存中…(img-jj3LqwYF-1689141095354)]
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<transition name="why">
<h2 v-if="isShow">
要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏。不能说,也不能想,却又不能忘。它们不能变成语言,它们无法变成语言,一旦变成语言就不再是它们了。它们是一片朦胧的温馨与寂寥,是一片成熟的希望与绝望,它们的领地只有两处:心与坟墓。比如说邮票,有些是用于寄信的,有些仅仅是为了收藏。
</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(false)
</script>
<style scoped>
h2 {
display: inline-block;
}
.why-enter-active {
animation: whyAnim 2s ease;
}
.why-leave-active {
/* animation: whyLeaveAnim 2s ease; */
animation: whyAnim 2s ease reverse;
}
@keyframes whyAnim {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes whyLeaveAnim {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(-500px);
opacity: 0;
}
}
</style>
同时设置过渡和动画(一般不设置)
◼ Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:
如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听;
◼ 但是如果我们同时使用了过渡和动画呢?
并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束;
在这种情况下,我们可以设置 type 属性为 animation 或者 transition 来明确的告知Vue监听的类型;
[外链图片转存中…(img-SnhIaD64-1689141095354)]
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<transition name="why">
<h2 v-if="isShow">
要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏。不能说,也不能想,却又不能忘。它们不能变成语言,它们无法变成语言,一旦变成语言就不再是它们了。它们是一片朦胧的温馨与寂寥,是一片成熟的希望与绝望,它们的领地只有两处:心与坟墓。比如说邮票,有些是用于寄信的,有些仅仅是为了收藏。
</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(false)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
显示指定动画时间
◼ 我们也可以显示指定过渡的时间,通过 duration 属性。
◼ duration可以设置两种类型的值:
number类型:同时设置进入和离开的过渡时间;
object类型:分别设置进入和离开的过渡时间;
[外链图片转存中…(img-PJ5vYpck-1689141095354)]
过渡的模式mode
[外链图片转存中…(img-WUu2XvtR-1689141095354)]
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<!-- mode属性掌握 -->
<transition name="why" mode="out-in">
<h2 v-if="isShow">哈哈哈</h2>
<h2 v-else>呵呵呵</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isShow = ref(true)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
动态组件的切换
[外链图片转存中…(img-Lr49iref-1689141095355)]
<template>
<div class="app">
<div>
<button @click="isShow = !isShow">切换</button>
</div>
<!-- mode属性掌握 -->
<transition name="why" mode="out-in" appear="">
<component :is=" isShow ? 'home': 'about'"></component>
</transition>
</div>
</template>
<script>
import Home from './pages/Home.vue'
import About from './pages/About.vue'
export default {
components: {
Home,
About
}
}
</script>
<script setup>
import { ref } from 'vue';
const isShow = ref(true)
</script>
<style scoped>
h2 {
display: inline-block;
}
/* transition */
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active {
animation: whyAnim 2s ease;
transition: opacity 2s ease;
}
.why-leave-active {
animation: whyAnim 2s ease reverse;
transition: opacity 2s ease;
}
@keyframes whyAnim {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
appear初次渲染
[外链图片转存中…(img-w9AmQ1Db-1689141095355)]
认识列表的过渡
◼ 目前为止,过渡动画我们只要是针对单个元素或者组件的:
要么是单个节点;
要么是同一时间渲染多个节点中的一个;
◼ 那么如果希望渲染的是一个列表,并且该列表中添加删除数据也希望有动画执行呢?
这个时候我们要使用 组件来完成;
◼ 使用 有如下的特点:
默认情况下,它不会渲染一个元素的包裹器,但是你可以指定一个元素并以 tag attribute 进行渲染;
过渡模式不可用,因为我们不再相互切换特有的元素;
内部元素总是需要提供唯一的 key attribute 值;
CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身;
[外链图片转存中…(img-H2FyWSv3-1689141095355)]
[外链图片转存中…(img-7xU0nOBJ-1689141095355)]
<template>
<div class="app">
<button @click="addNumber">添加数字</button>
<button @click="removeNumber">删除数字</button>
<button @click="shuffleNumber">打乱数字</button>
<transition-group tag="div" name="why">
<template v-for="item in nums" :key="item">
<span>{
{ item }}</span>
</template>
</transition-group>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { shuffle } from "underscore";
const nums = ref([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
const addNumber = () => {
nums.value.splice(randomIndex(), 0, nums.value.length)
}
const removeNumber = () => {
nums.value.splice(randomIndex(), 1)
}
const shuffleNumber = () => {
nums.value = shuffle(nums.value)
}
const randomIndex = () => {
return Math.floor(Math.random() * nums.value.length)
}
</script>
<style scoped>
span {
margin-right: 10px;
display: inline-block;
}
.why-enter-from,
.why-leave-to {
opacity: 0;
transform: translateY(30px);
}
.why-enter-to,
.why-leave-from {
opacity: 1;
transform: translateY(0);
}
.why-enter-active,
.why-leave-active {
transition: all 2s ease;
}
.why-leave-active {
position: absolute;
}
/* 针对其他移动的阶段需要的动画 */
.why-move {
transition: all 2s ease;
}
</style>