一 watch侦听器
watch侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监听用户名的变化并发起请求,判断用户名是否可用。
1.1 watch侦听器的基础用法
开发者需要在watch节点下,定义自己的侦听器。如下:
<template>
<div>
<h3>watch</h3>
<input type="text" class="form-control" v-model.trim="username">
</div>
</template>
<script>
export default {
name:'Mywatch',
data(){
return{
username:''
}
},
watch:{
username(newVal,oldVal){
console.log(newVal,oldVal)
}
}
}
</script>
1.2 immediate选项
默认情况下,组件在初次加载完毕后不会调用watch侦听器,如果想让watch侦听器立即被调用,则需要使用immediate选项。如下:
watch:{
username:{
async handler(newVal,oldVal){
const res = await axios.get('https://applet-base-api-t.itheima.net/api/finduser'+newVal)
console.log(res)
},
immediate:true
}
}
1.3 deep选项
当watch侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用deep选项,如下:
<template>
<div>
<h3>watch</h3>
<input type="text" class="form-control" v-model.trim="info.username">
</div>
</template>
<script>
import axios from 'axios'
export default {
name:'Mywatch',
data(){
return{
username:'admin',
info:{
username:'zs'
}
}
},
watch:{
info:{
async handler(newVal){
const {data:res} = await axios.get('https://applet-base-api-t.itheima.net/api/finduser/'+newVal.username)
console.log(res)
},
deep:true
}
}
}
</script>
1.4 监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下方式定义watch侦听器
<template>
<div>
<h3>watch</h3>
<input type="text" class="form-control" v-model.trim="info.username">
</div>
</template>
<script>
import axios from 'axios'
export default {
name:'Mywatch',
data(){
return{
username:'admin',
info:{
username:'zs'
}
}
},
watch:{
'info.username':{
async handler(newVal){
const {data:res} = await axios.get('https://applet-base-api-t.itheima.net/api/finduser/'+newVal)
console.log(res)
},
deep:true
}
}
}
</script>
1.5 计算属性VS侦听器
计算属性和侦听器侧重的应用场景不同:
计算属性侧重于监听多个数值的变化,最终计算并返回一个新值
侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
二 组件的生命周期
2.1 组件运行的过程
组件的生命周期指的是:组件从创建——运行(渲染)——销毁的整个过程,强调的是一个时间段。
2.2 如何监听组件的不同时刻
vue框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。例如:
当组件在内存中被创建完毕后,会自动调用created函数
当组件被成功的渲染到页面上之后,会自动调用mounted函数
当组件被销毁完毕后,会自动调用unmounted函数
App.vue
<template>
<div>
app根组件
<button @click="flag=!flag">显示或隐藏</button>
<life-cycle></life-cycle>
</div>
</template>
<script>
import LifeCycle from './LifeCycle.vue'
export default {
name:'MyApp',
data(){
return {
flag:true
}
},
components:{
LifeCycle
}
}
</script>
LifeCycle.vue
<template>
<div>
life
</div>
</template>
<script>
export default {
name:'LifeCycle',
// 组件在内存中被创建完毕了
created(){
console.log('created:组件在内存中被创建完毕了')
},
// 组建第一次被渲染到了页面上
mounted(){
console.log('mounted: 组建第一次被渲染到了页面上')
},
// 组件被销毁完毕了
unmounted(){
console.log('unmounted:组件被销毁完毕了')
}
}
</script>
2.3 如何监听组件的更新
当组件的data数据更新之后,vue会自动重新渲染组件的DOM结构,从而保证View视图展示的数据和Model数据源保持一致。
当组件被重新渲染完毕之后,会自动调节updated生命周期函数。
<template>
<div>
life---{{count}}
<button @click="count+=1">+1</button>
</div>
</template>
<script>
export default {
name:'LifeCycle',
data(){
return {
count:0
}
},
// 组件在内存中被创建完毕了
created(){
console.log('created:组件在内存中被创建完毕了')
},
// 组建第一次被渲染到了页面上
mounted(){
console.log('mounted: 组建第一次被渲染到了页面上')
},
// 组件被重新渲染完毕了
updated(){
console.log('updated:组件被重新渲染完毕了');
},
// 组件被销毁完毕了
unmounted(){
console.log('unmounted:组件被销毁完毕了')
}
}
</script>
2.4 组件中主要的生命周期函数
在实际开发中,created是最常用的生命周期函数!
2.5 组件中全部的生命周期函数
三 组件之间的数据共享
3.1 组件之间的关系
在项目开发中,组件之间的关系分为如下3种:
- 父子关系
- 兄弟关系
- 后代关系
3.2 父子组件之间的数据共享
3.2.1 父组件向子组件共享数据
父组件通过v-bind属性绑定向子组件共享数据。同时,子组件需要使用props接收数据。如下:
父组件App.vue
<template>
<div>
app---{{count}}
<button type="button" class="btn btn-primary" @click="count+=1">+1</button>
<my-son :num="count"></my-son>
</div>
</template>
<script>
import MySon from './Son.vue'
export default {
name:'MyApp',
data(){
return {
count:0
}
},
components:{
MySon
}
}
</script>
<style>
</style>
子组件Son.vue
<template>
<div>
son---{{num}}
</div>
</template>
<script>
export default {
name:'MySon',
props:['num']
}
</script>
3.2.2 子组件向父组件共享数据
子组件通过自定义事件的方式向父组件共享数据,示例如下:
父组件App.vue
<template>
<div>
app---{{count}}
<button type="button" class="btn btn-primary" @click="count+=1">+1</button>
<my-son :num="count" @numchange="getNum"></my-son>
</div>
</template>
<script>
import MySon from './Son.vue'
export default {
name:'MyApp',
data(){
return {
count:0
}
},
methods:{
getNum(num){
this.count=num
}
},
components:{
MySon
}
}
</script>
子组件Son.vue
<template>
<div>
son---{{num}}
<button type="button" class="btn btn-primary" @click="add">+1</button>
</div>
</template>
<script>
export default {
name:'MySon',
props:['num'],
emits:['numchange'],
methods:{
add(){
this.$emit('numchange',this.num+1)
}
}
}
</script>
3.2.3 父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用v-model指令维护组件内外数据的双向同步:
<template>
<div>
app---{{count}}
<button type="button" class="btn btn-primary" @click="count+=1">+1</button>
<hr>
<my-son v-model:num="count"></my-son>
</div>
</template>
<script>
import MySon from './Son.vue'
export default {
name:'MyApp',
data(){
return {
count:0
}
},
methods:{
getNum(num){
this.count=num
}
},
components:{
MySon
}
}
</script>
<template>
<div>
son---{{num}}
<button type="button" class="btn btn-primary" @click="add">+1</button>
</div>
</template>
<script>
export default {
name:'MySon',
props:['num'],
emits:['update:num'],
methods:{
add(){
this.$emit('update:num',this.num+1)
}
}
}
</script>
3.3 兄弟组件之间的数据共享
兄弟组件之间实现数据共享的方案是EventBus。可以借助于第三方的包mitt来创建eventBus对象,从而实现兄弟组件之间的数据共享。
第一步 安装mitt依赖包 npm install mitt
第二步 创建公共的EventBus模块 创建eventBus.js文件,代码如下
import mitt from 'mitt'
const bus =mitt()
export default bus
第三步 在数据接收方自定义事件 在数据接收方调用bus.on(‘事件名称’,事件处理函数)方法注册一个自定义事件。代码如下:
<template>
<div>
<h3>数据接收方——————num的值为:{{ num }}</h3>
</div>
</template>
<script>
import bus from './eventBus.js'
export default {
name:"MyRight",
data(){
return{
num:0
}
},
created(){
// 调用bus.on()方法注册一个自定义事件,通过事件处理函数的形参接收数据
bus.on('countChange',count=>{
this.num=count
})
}
}
</script>
第四步 在数据发送方触发事件 在数据发送方,调用bus.emit('事件名称',要发送的数据)方法触发自定义事件,代码如下
<template>
<div>
<h3>数据发送方----count的值为:{{ count }}</h3>
<button type="button" class="btn btn-primary" @click="add">+1</button>
</div>
</template>
<script>
import bus from './eventBus.js'
export default {
name:"MyLeft",
data(){
return {
count:0
}
},
methods:{
add(){
this.count++,
bus.emit('countChange',this.count)
}
}
}
</script>
最后附上App根组件App.vue的代码,如下:
<template>
<div>
App
<div class="brother-box">
<!-- 3. 使用组件 -->
<my-left></my-left>
<my-right></my-right>
</div>
</div>
</template>
<script>
import MyLeft from './Left.vue'
import MyRight from './Right.vue'
export default {
name:'MyApp',
components:{
MyLeft,
MyRight
}
}
</script>
<style lang="less" scoped>
.brother-box {
display: flex;
> div {
border: 1px solid #ccc;
flex: 1;
margin: 10px;
padding: 10px;
height: 300px;
}
}
</style>
实现效果:
点击左侧按钮,count值+1,并且num值=count值
3.4 后代关系组件之间的数据共享
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用provide和inject实现后代关系组件之间的数据共享。
3.4.1 父节点通过provide共享数据
父节点的组件可以通过provide方法,对其子孙组件共享数据:
<template>
<div>
<h1>App根组件</h1>
<hr>
<level-two></level-two>
</div>
</template>
<script>
import LevelTwo from './LevelTwo.vue'
export default {
name:"MyApp",
data(){
return{
color:'red'
}
},
provide(){
// 返回要共享的数据对象
return {
color:this.color,
count:1
}
},
components:{
LevelTwo
}
}
</script>
3.4.2 子孙节点通过inject接收数据
子孙节点可以使用inject数组,接收父级节点向下共享的数据。示例代码如下:
子组件
<template>
<div>
<h3>level-two二级组件</h3>
<hr>
<level-three></level-three>
</div>
</template>
<script>
import LevelThree from './LevelThree.vue'
export default {
name:'LevelTwo',
components:{
LevelThree
}
}
</script>
孙组件
<template>
<div>
<h5>LevelThree三级组件---{{ color }}--{{ count }}</h5>
</div>
</template>
<script>
export default {
name:"LevelThree",
inject:['color','count'],
}
</script>
<style>
</style>
3.4.3父节点对外共享响应式的数据
父节点使用provide向下共享数据时,可以结合computed函数向下共享响应式的数据。如下
<template>
<div>
<h1>App根组件</h1>
<button @click="color='blue'">change-color</button>
<hr>
<level-two></level-two>
</div>
</template>
<script>
import { computed } from 'vue';
import LevelTwo from './LevelTwo.vue'
export default {
name:"MyApp",
data(){
return{
color:'red'
}
},
provide(){
// 返回要共享的数据对象
return {
// 使用computed函数,可以把要共享的数据包装为响应式数据
color:computed(()=>this.color),
count:1
}
},
components:{
LevelTwo
}
}
</script>
3.4.4 子孙节点使用响应式的数据
如果父节点共享的是响应式的数据,则子孙节点必须以.value的形式进行使用。如下
<template>
<div>
<h5>LevelThree三级组件---{{color.value}}</h5>
</div>
</template>
<script>
export default {
name:"LevelThree",
inject:['color'],
}
</script>
3.4.5 vuex
vuex是终极的组件之间的数据共享方案。在企业级的vue项目开发中,vuex可以让组件之间的数据共享变得高效清晰且易于维护。
四、vue3.x中全局配置axios
4.1 为什么要全局配置axios
在实际项目开发中,几乎每个组件中都会用到axios发起数据请求。此时会遇到如下两个问题:
每个组件中都需要导入axios
每次发请求都需要填写完整的请求路径
4.2 如何全局配置axios
在main.js入口文件中,通过app.config.globalProperties全局挂载axios,如下:
import { createApp } from 'vue'
import App from './components/06.network/App.vue'
import './index.css'
import axios from 'axios'
const app=createApp(App)
axios.default.baseURL='https://www.escook.cn'
app.config.globalProperties.$http=axios
app.mount('#app')
发起请求
<template>
<div>
<h3>post</h3>
<hr>
<button type="button" class="btn btn-success" @click="PostInfo">发起post请求</button>
</div>
</template>
<script>
export default {
name:'PostInfo',
methods:{
async PostInfo(){
const {data:res}=await this.$http.post('/api/post',{name:'zs',age:20})
console.log(res)
}
}
}
</script>
五、ref引用
5.1 什么是ref引用
ref用来辅助开发者在不依赖于jQuery的情况下,获取DOM元素或组件的引用。
每个vue的组件实例上,都包含一个$refs对象,里面存储着对应的DOM元素或组件的引用。默认情况下,组件的$refs指向一个空对象。
5.2 使用ref引用DOM元素
如果想要使用ref引用页面上的DOM元素,则可以按照如下方式操作:
<template>
<div>
<h1 ref="myh1">App根组件</h1>
<hr>
<button type="button" class="btn btn-primary" @click="getRefs">获取$refs引用</button>
</div>
</template>
<script>
export default {
name:'MyApp',
methods:{
getRefs(){
// console.log(this.$refs)
this.$refs.myh1.style.color='red'
}
}
}
</script>
5.3 使用ref引用组件的实例
如果想要使用ref引用页面上的组件实例,可以按照如下方式操作:
<template>
<div>
<h1 ref="myh1">App根组件</h1>
<hr>
<button type="button" class="btn btn-primary" @click="getRefs">获取$refs引用</button>
<my-counter ref="counterRef"></my-counter>
</div>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name:'MyApp',
methods:{
getRefs(){
// console.log(this.$refs)
// this.$refs.myh1.style.color='red'
this.$refs.counterRef.reset()
}
},
components:{
MyCounter
}
}
</script>
<template>
<div class="counter-container">
<h3>Counter组件---{{ count }}</h3>
<hr>
<button type="button" class="btn btn-info" @click="count+=1">+1</button>
</div>
</template>
<script>
export default {
name:'MyCounter',
data(){
return{
count:0
}
},
methods:{
reset(){
this.count=0
}
}
}
</script>
<style lang="less" scoped>
.counter-container {
margin: 20px;
padding: 20px;
border: 1px solid #efefef;
border-radius: 4px;
box-shadow: 0px 1px 10px #efefef;
}
</style>
5.4 组件是异步执行DOM更新的
<template>
<div>
<h1>App</h1>
<hr>
<input ref="ipt" type="text" class="from-control" v-if="inputVisible">
<button type="button" class="btn btn-primary" v-else @click="showInput">展示input输入框</button>
</div>
</template>
<script>
export default {
name:'MyApp',
data(){
return{
inputVisible:false
}
},
methods:{
showInput(){
this.inputVisible=true
this.$refs.ipt.focus()
}
}
}
</script>
<style lang="less" scoped>
input.form-contorl{
width: 280px;
display: inline;
}
</style>
以上代码在点击按钮后,并不能执行focus()方法,因为this.$refs.ipt是undefined,在执行到该步骤时,input还没有被渲染
5.5 了解$nextTick函数的作用
组件的$nextTick(cb)方法,会把cb回调推迟到下一个DOM更新周期之后执行。通俗的说:等组件的DOM异步地重新渲染完成后,再执行cb回调函数。从而保证cb回调函数可以操作到最新的DOM元素。将5.4中的showInput方法作如下修改,解决报错问题。
methods:{
showInput(){
this.inputVisible=true
this.$nextTick(()=>{
this.$refs.ipt.focus()
})
}
}
六、动态组件
6.1 什么是动态组件
动态组件指的是动态切换组件的显示与隐藏。vue提供了一个内置的<component>组件,专门用来实现组件的动态渲染。
<component>是组件的占位符
通过is属性动态指定要渲染的组件名称
<component is="要渲染的组件名称"></component>
<template>
<div>
<h1 class="mb-4">App根组件</h1>
<button type="button" class="btn btn-primary" @click="comName='MyHome'">首页</button>
<button type="button" class="btn btn-info m1-2" @click="comName='MyMovie'">电影</button>
<hr>
<!-- <my-home></my-home>
<my-movie></my-movie> -->
<component :is="comName"></component>
</div>
</template>
<script>
import MyHome from './Home.vue'
import MyMovie from './Movie.vue'
export default {
name:'MyApp',
data(){
return{
comName:'MyHome'
}
},
components:{
MyHome,
MyMovie
}
}
</script>
<template>
<div>
<h3>Movie组件---{{ count }}</h3>
<button type="button" class="btn btn-primary" @click="count+=1">+1</button>
</div>
</template>
<script>
export default {
name:'MyMovie',
data(){
return{
count:0
}
}
}
</script>
6.2 使用keep-alive保持组件状态
默认情况下,切换动态组件时无法保持组件的状态。此时可以使用vue内置的<keep-alive>组建保持动态组件的状态。如下:
<keep-alive>
<component :is="comName"></component>
</keep-alive>
七 插槽
7.1 什么是插槽
插槽(Slot)是vue为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。
7.2 体验插槽的基础用法
在封装组件时,可以通过<slot>元素定义插槽,从而为用户预留内容占位符。示例:
<template>
<div class="com-container">
<h3>MyCom组件----插槽的基础用法</h3>
<hr>
<p>这是第一个p标签</p>
//定义插槽
<slot></slot>
<p>这是最后一个p标签</p>
</div>
</template>
<script>
export default {
name:'MyCom'
}
</script>
<template>
<div>
<h1>App根组件</h1>
<hr>
<my-com>
//用户定义的插槽内容
<p>--------</p>
</my-com>
</div>
</template>
<script>
import MyCom from './MyCom.vue'
export default {
components: { MyCom },
}
7.2.1 没有预留插槽的内容会被丢弃
如果在封装组件时没有预留任何<slot>插槽,则用户提供的任何自定义内容都会被丢弃。
如果上述代码中将插槽注释,则用户定义的<p>-----</p>不会显示:
7.2.2 后备内容
封装组件时,可以为预留的<slot>插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例如下:在插槽中输入后备内容,在使用时不提供插槽内容
<template>
<div class="com-container">
<h3>MyCom组件----插槽的基础用法</h3>
<hr>
<p>这是第一个p标签</p>
<slot>这是后备内容</slot>
<p>这是最后一个p标签</p>
</div>
</template>
<template>
<div>
<h1>App根组件</h1>
<hr>
<my-com>
<!-- <p>--------</p> -->
</my-com>
</div>
</template>
7.3 具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个<slot>插槽指定具体的name名称。这种带有具体名称的插槽叫做“具名插槽”。没有指定name名称的插槽,会有隐含的名称叫做“default”。
<template>
<div>
<!-- 我们希望把页头放到这里 -->
<header>
<slot name="header"></slot>
</header>
<!-- 我们希望把主要内容放到这里 -->
<main>
<slot></slot>
</main>
<!-- 我们希望把页脚放到这里 -->
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
name: 'MyArticle',
}
</script>
<style lang="less" scoped>
header,
main,
footer {
border: 1px dashed #999;
margin: 30px;
padding: 40px;
position: relative;
&::before {
position: absolute;
right: 5px;
top: 5px;
font-style: italic;
}
}
header::before {
content: 'header 页头';
}
main::before {
content: 'main 主体内容';
}
footer::before {
content: 'footer 页脚';
}
</style>
7.3.1 为具名插槽提供内容
想具名插槽提供内容的时候,我们可以在一个<template>元素上使用v-slot指令,并以v-slot的参数的形式提供其名称。
<my-article>
<template v-slot:header>
<h1>滕王阁序</h1>
</template>
</my-article>
</div>
</template>
7.3.2 具名插槽的简写形式
把参数之前的所有内容(v-solt:)替换为字符#。例如v-slot:header可以被重写为#header
<template>
<div>
<h1>App 根组件</h1>
<hr />
<!-- 使用组件 -->
<my-article>
<template #header>
<h1>滕王阁序</h1>
</template>
<template #default>
<p>豫章故郡,洪都新府。</p>
<p>星分翼轸,地接衡庐</p>
<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
</template>
<template #footer>
<p>落款:王勃</p>
</template>
</my-article>
</div>
</template>
<script>
// 导入组件
import MyArticle from './MyArticle.vue'
export default {
name: 'MyApp',
components: {
// 注册组件
MyArticle,
},
}
</script>
7.4 作用域插槽
在封装组件的过程中,可以为预留的<slot>插槽绑定props数据,这种带有props数据的<slot>叫做“作用域插槽”。
<template>
<div>
<h3>Test</h3>
<slot :info="infomation"></slot>
</div>
</template>
<script>
export default {
name:"MyTest",
data(){
return{
infomation:{
phone:'1301216666',
address:'北京'
}
}
}
}
</script>
<template>
<div>
<h1>App根组件</h1>
<my-test>
<template v-slot:default="scope">
<p>{{scope.info.address}}</p>
</template>
</my-test>
</div>
</template>
<script>
import MyTest from './MyTest.vue'
export default {
components: {MyTest},
}
</script>
7.4.1 解构作用域插槽的prop
<my-test>
<template v-slot:default="{info}">
<p>{{info}}</p>
</template>
</my-test>
八 自定义指令
8.1 什么是自定义指令
vue提供了v-for v-model等内置指令,除此之外vue还允许开发者自定义指令。
vue中的自定义指令分两类:私有自定义指令,全局自定义指令
8.2 声明私有自定义指令的语法
在每个vue组件中,可以在directives节点下声明私有自定义指令。如下:
<template>
<div class="home-container">
<h3 v-color="'red'">MyHome 组件 --- {{ count }}</h3>
<hr />
<input type="text" class="form-control" v-focus />
</div>
</template>
<script>
export default {
name: 'MyHome',
directives: {
// 自定义私有指令
focus: {
// 当被绑定的元素插入到DOM中时,自动触发mounted函数
mounted(el) {
el.focus() //让被绑定的元素自动获得焦点
},
},
},
}
</script>
8.3 声明全局自定义指令
全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明,(在main.js中)示例代码如下:
import { createApp } from 'vue'
import App from './components/07.directive/App.vue'
import './index.css'
import './assets/css/bootstrap.css'
const app=createApp(App)
app.directive('focus',{
mounted(el){
el.focus()
}
})
app.mount('#app')
8.4 updated函数
mounted函数只在元素第一次插入DOM时被调用,当DOM更新时mounted函数不会被触发。updated函数会在每次DOM更新完成后被调用。示例:
import { createApp } from 'vue'
import App from './components/07.directive/App.vue'
import './index.css'
import './assets/css/bootstrap.css'
const app=createApp(App)
app.directive('focus',{
mounted(el){
el.focus()
},
updated(el){
el.focus()
}
})
app.mount('#app')
注意:在vue2的项目中使用自定义指令时,mounted->bind updated->update
8.5 函数简写
如果mounted和updated函数中的逻辑完全相同,可以简写成如下格式:
app.directive('focus',(el)=>{
el.focus()
})
8.6 指令的参数值
在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值,示例代码:
import { createApp } from 'vue'
import App from './components/07.directive/App.vue'
import './index.css'
import './assets/css/bootstrap.css'
const app=createApp(App)
app.directive('focus',(el)=>{
el.focus()
})
app.directive('color',(el,binding)=>{
el.style.color=binding.value
})
app.mount('#app')
<template>
<div class="home-container">
<h3 v-color="'red'">MyHome 组件 --- {{ count }}</h3>
<hr />
<input type="text" class="form-control" v-focus v-color="'cyan'" />
<button type="button" class="btn btn-primary" @click="count += 1">+1</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
data() {
return {
count: 0,
}
},
}
</script>