四、Vue 中的动画
本章中,将会通过 讲解单元素单组件 的 动画 ,与 多元素的切换动画 ,以及 状态动画 等,帮助大家了解在 Vue 中,如何借助一定的 封装 ,实现 CSS 和 JS动画 ,让大家能够快速实现 酷炫 的 动态效果。
1.使用 Vue 实现基础的 CSS 过渡与动画效果
讲一下 动画 以及 过渡 的基本使用,这里主要是用 class 写好样式,用vue动态的添加 class ,或者动态的添加 style 来实现动画的效果。
动画效果
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用 Vue 实现基础的 CSS 过渡与动画效果</title>
<style>
/* 定义动画 */
@keyframes leftToRight {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(0px);
}
}
.animation{
animation: leftToRight 3s ;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 过渡(红色变绿色这个过程叫过渡),动画(让一个元素从上到下,来回做一个弹跳)
const app = Vue.createApp({
data(){
return {
animate: {
animation: true // animation名称class
}
}
},
methods: {
handleClick(){
this.animate.animation = !this.animate.animation
}
},
template: `
<div :class="animate">hello world</div>
<button @click="handleClick">切换</button>
`
})
const vm = app.mount('#root')
</script>
</html>
过渡
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用 Vue 实现基础的 CSS 过渡与动画效果</title>
<style>
.transition{
transition: 3s background-color ease;
}
.blue {
background-color: blue;
}
.green {
background-color: green;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 过渡(红色变绿色这个过程叫过渡),动画(让一个元素从上到下,来回做一个弹跳)
const app = Vue.createApp({
data(){
return {
animate: {
transition: true,
blue: true,
green: false
}
}
},
methods: {
handleClick(){
this.animate.blue = !this.animate.blue
this.animate.green = !this.animate.green
}
},
template: `
<div :class="animate">hello world</div>
<button @click="handleClick">切换</button>
`
})
const vm = app.mount('#root')
</script>
</html>
或者用 style 的方式实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用 Vue 实现基础的 CSS 过渡与动画效果</title>
<style>
.transition{
transition: 3s background-color ease;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 过渡(红色变绿色这个过程叫过渡),动画(让一个元素从上到下,来回做一个弹跳)
const app = Vue.createApp({
data(){
return {
styleObj: {
background: 'blue'
}
}
},
methods: {
handleClick(){
if(this.styleObj.background === 'blue') {
this.styleObj.background = 'green'
} else {
this.styleObj.background = 'blue'
}
}
},
template: `
<div class="transition" :style="styleObj">hello world</div>
<button @click="handleClick">切换</button>
`
})
const vm = app.mount('#root')
</script>
</html>
2.使用 transition 标签实现单元素组件的过渡和动画效果
接下来讲解一下 vue 里面关于 动画 和 过渡效果 的一些 封装 ,使用这些封装 , 能让我们更加的方便去编写 vue 里面的 动画 和 过渡效果 。
单组件的入场 / 出场动画(过渡效果)
单元素 或 单组件 的 入场 / 出场 动画,只控制一个 元素(或组件) ,的显示隐藏 状态,就叫做 单元素 或 单组件 ,元素从 展示变成隐藏这就叫做出场 ,从 隐藏变成展示就叫做入场 , vue 给我们提供了 transition 标签,我们可以通过 transition 标签来做 入场、出场动画 。
transition 需要配合着一些 样式 才会好用,具体需要哪些样式呢,如下:
| class类名 | 说明 |
|---|---|
| 入场动画: | |
| .v-enter-from | 入场效果, 最初展示的模样 |
| .v-enter-active | 入场动画的 过渡(过程) |
| .v-enter-to | 入场 动画结束 时候 |
| 出场动画: | |
| .v-leave-from | 出场效果 |
| .v-leave-active | 出场动画的过渡(过程) |
| .v-leave-to | 入场 动画结束 时候 |
使用案例如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>单组件的入场 / 出场动画</title>
<style>
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from {
opacity: 0;
}
/* 入场动画的过渡(过程) */
/* .v-enter-active {
transition: opacity 3s ease-out; // 单独写可以自定义过渡效果的不同
}*/
/* 结束时候 */
.v-enter-to {
opacity: 1;
}
/* --------------------出场动画----------------------- */
/* 出场效果(可以不写,因为出场动画出现的一瞬间本身就是显示的状态)*/
.v-leave-from {
opacity: 1;
}
/* 出场动画的过渡(过程) */
/* .v-leave-active {
transition: opacity 3s ease-in; // 单独写可以自定义过渡效果的不同
} */
/* 结束时候 */
.v-leave-to {
opacity: 0;
}
/* --------------------上面的.v-enter-active .v-leave-active 可以放在一起写----------------------- */
.v-enter-active,
.v-leave-active{
transition: opacity 3s ease-out;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
动画效果
动画只需要依赖 .v-enter-active 与 .v-leave-active 以及一个写好的 @keyframes 关键帧动画,即可完成一个动画的效果。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动画效果</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
animation: shake 3s;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
给class重命名
上面的 动画类名 都是 .v-enter-to 或 v-enter-from,都是以 v 开头 ,其实我们是可以给这个 class 重命名 的,只需要给 transition标签 , 添加一个 name属性 即可。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>给class重命名</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场动画的过渡(过程) */
.hello-enter-active {
animation: shake 3s;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.hello-leave-active {
animation: shake 3s;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition name="hello">
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
注意: 上面的动画不仅可以使用 v-if 来控制, 也可以使用 v-show 。
自定义class
同时这个 transition标签 的 class 也支持 自定义class ,自己来依自己的习惯叫自己喜欢的名字,例如想修改 .v-enter-active 这个类名,只需要在 transition标签 上添加 enter-active-class 属性来自定义类名即可
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义class</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场动画的过渡(过程) */
.hello {
animation: shake 3s;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.bay {
animation: shake 3s;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition
enter-active-class="hello"
leave-active-class="bay"
>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
自定义class结合animate.css使用
animate.css 的具体使用教程,请点击这里
首先 通过cdn 的方式引入 animate.css ,在使用animate.css中的 任何class动画类名 都需要与 animate__animated 类名一同使用,而且配合使用 自定义class ,可以很方便的 和第三方的动画库相结合 ,例子如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义class结合animate.css使用</title>
<!-- 引入animate.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition
enter-active-class="animate__animated animate__backInDown"
leave-active-class="animate__animated animate__flash"
>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
过渡与动画同时触发
有些时候我们 不仅希望只有在显示隐藏元素的瞬间有过渡的效果,我们同时希望有动画的效果 ,我们可能还想让 字体颜色 也有改变的效果,或者 坐标位置 有所改变,等等一系列更多的效果,下面的代码在有 过渡 的同时候也改变了 字体颜色 。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>过渡与动画同时触发</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
transition: color 3s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 3s;
transition: all 3s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
根据animation或transition来决定动画执行时间
接下来看我们的代码中 animation 的过渡效果是 10s , transition 的动画效果是 3s,这样动画就会出现 3s 的先执行结束,然后漫长的等 10s 的过渡效果
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>根据animation或transition来决定动画执行时间</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 10s;
transition: color 3s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 10s;
transition: all 3s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
假如我们有一个需求,想让 过渡时间与动画时间相同,或动画时间与过渡时间相同 ,该怎么做呢? 我们只需要在 transition标签 上添加一个 type 属性, type 属性等于 transition 时,就是 以过渡效果的时间为准 , type 属性等于 animation 时,就是 以动画效果的时间为准 ,代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>根据animation或transition来决定动画执行时间</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
transition: color 10s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 3s;
transition: color 10s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition type="animation">
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
duration(持续时间)属性
像上面的 根据animation或transition来决定动画执行时间例子 很不易懂,我们通过使用 duration属性实现同样的效果
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>duration(持续时间)属性</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
transition: color 10s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 3s;
transition: color 10s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition :duration="1000">
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
:duration="1000" 的意思就是不论是 过渡时间 还是 动画时间 ,都执行 1000毫秒(1秒) ,虽然 css样式 中写了 3s 与 10s ,我只执行 duration 属性设置的 1s 。
duration属性 的值也可以设置为 对象形式 , :duration="{ enter:1000, leave: 3000 }" 意思是 入场动画1秒,出场动画3秒 。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>duration(持续时间)属性</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
transition: color 10s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 3s;
transition: color 10s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition :duration="{ enter:1000, leave: 3000 }">
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
用 js 做动画
我们上面讲解的,还是 使用css做动画 ,那我们如何使用 js 去做动画呢,首先在 transition标签 上添加 :css="false" ,意思就是 不使用 transition 来做 css动画 ,然后做 js动画的话, vue 提供给了我们一些 钩子(某些时刻会自动调用的函数) ,如下表格:
| 钩子名称 | 钩子说明 | 回调参数说明 |
|---|---|---|
| 入场动画钩子: | ||
| @before-enter | 进入入场动画之前时 | 参数1:el:vue返回的dom元素 |
| @enter | 开始执行入场动画时 | 参数1:el:vue返回的dom元素 参数2:done: done方法可以停止动画 |
| @after-enter | 入场动画结束时 | 参数1:el:vue返回的dom元素 |
| 出场动画钩子: | ||
| @before-leave | 出场动画之前时 | 参数1:el:vue返回的dom元素 |
| @leave | 开始执行出场动画时 | 参数1:el:vue返回的dom元素 参数2:done: done方法可以停止动画 |
| @after-leave | 出场动画结束时 | 参数1:el:vue返回的dom元素 |
具体例子使用方法如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>duration(持续时间)属性</title>
<style>
/* 关键帧 */
@keyframes shake {
0% {
transform: translateX(-100px);
}
50% {
transform: translateX(-50px);
}
100% {
transform: translateX(50px);
}
}
/* --------------------入场动画----------------------- */
/* 入场效果*/
.v-enter-from{
color: red;
}
/* 入场动画的过渡(过程) */
.v-enter-active {
animation: shake 3s;
transition: color 10s ease-in;
}
/* --------------------出场动画----------------------- */
/* 出场动画的过渡(过程) */
.v-leave-active {
color: red;
animation: shake 3s;
transition: color 10s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 单元素,单组件的入场出场动画
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleClick(){
this.show = !this.show
},
/**
* 进入入场动画之前时
* @param {Object} el - vue传递过来的dom元素
*/
// ------------------------入场动画---------------------------
handleBeforeEnter(el){
el.style.color = 'red'
},
/**
* 开始执行入场动画时
* @param {Object} el - vue传递过来的dom元素
* @param {Funcion} done - done方法可以停止动画
*/
handleEnterActive(el, done){
// 执行动画
const animate = setInterval(() => {
const color = el.style.color
if(color === 'red') {
el.style.color = 'green'
} else {
el.style.color = 'red'
}
}, 1000);
// 3秒后清除动画
setTimeout(() => {
clearInterval(animate)
// 1. 注意:如果不主动调用done方法,程序不会知道动画是否结束,
// 也不会去走【动画结束的钩子函数】
done()
}, 3000);
},
/**
* 入场动画结束时
*/
handleEnterEnd(){
// 2. 上面的方法中调用【done】后,才会执行该方法中的逻辑
alert('入场动画结束时')
},
// ------------------------出场动画---------------------------
/**
* 出场动画之前时
* @param {Object} el - vue传递过来的dom元素
*/
handleBeforeLeave(el){
alert('出场动画之前')
},
/**
* 开始执行出场动画时
* @param {Object} el - vue传递过来的dom元素
* @param {Funcion} done - done方法可以停止动画
*/
handleLeaveActive(el, done){
alert('开始执行出场动画')
done()
},
/**
* 出场动画结束时
*/
handleLeaverEnd(el){
alert('出场动画结束')
}
},
template: `
<div>
<transition
:css="false"
@before-enter="handleBeforeEnter"
@enter="handleEnterActive"
@after-enter="handleEnterEnd"
@before-leave="handleBeforeLeave"
@leave="handleLeaveActive"
@after-leave="handleLeaverEnd"
>
<div v-if="show">hello world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
3.组件和元素切换动画的实现
1.多元素的切换动画
之前上一章讲过单元素动画 ,这次讲一下 多元素的动画 ,实际上单元素与 多元素 写法都一样,代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组件和元素切换动画的实现</title>
<style>
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
}
.v-enter-active{
transition: opacity 3s ease-in;
}
.v-enter-to {
opacity: 1;
}
/* -------------------出场动画----------------------- */
.v-leave-from{
opacity: 1;
}
.v-leave-active{
transition: opacity 3s ease-in;
}
.v-leave-to {
opacity: 0;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
data(){
return { show: false }
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition>
<div v-if="show">hello world</div>
<div v-else="show">bye world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
上面虽然实现了 多元素动画 的效果,但是有一个问题:hello world 展示时候,bye world 慢慢悠悠的消失,导致每次切换时候能同时看到2个元素,如果想每次一个元素隐藏结束了,再显示另外一个元素 ,如何解决呢,只需要在 transition标签 上添加 mode="out-in" 就行,意思就是 先隐藏后展示,先出去再进来(in-out 先进再出 out-in 先出再进) , 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组件和元素切换动画的实现</title>
<style>
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
}
.v-enter-active{
transition: opacity 3s ease-in;
}
.v-enter-to {
opacity: 1;
}
/* -------------------出场动画----------------------- */
.v-leave-from{
opacity: 1;
}
.v-leave-active{
transition: opacity 3s ease-in;
}
.v-leave-to {
opacity: 0;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
data(){
return { show: false }
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition mode="out-in">
<div v-if="show">hello world</div>
<div v-else="show">bye world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
刚进入页面时候 bye world 是没有动画的,如果希望刚进入页面 bye world 就有一个动画效果,可以给 transition标签 添加一个 appear 属性,它的意思是 初次对某一个默认元素显示的时候会给它带上我们写的这个动画效果 ,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组件和元素切换动画的实现</title>
<style>
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
}
.v-enter-active{
transition: opacity 3s ease-in;
}
.v-enter-to {
opacity: 1;
}
/* -------------------出场动画----------------------- */
.v-leave-from{
opacity: 1;
}
.v-leave-active{
transition: opacity 3s ease-in;
}
.v-leave-to {
opacity: 0;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
data(){
return { show: false }
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition mode="out-in" appear>
<div v-if="show">hello world</div>
<div v-else="show">bye world</div>
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
2.多组件的切换动画
多组件 的切换没什么不同只是改成了 组件 的形式,代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组件和元素切换动画的实现</title>
<style>
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
}
.v-enter-active{
transition: opacity 1s ease-in;
}
.v-enter-to {
opacity: 1;
}
/* -------------------出场动画----------------------- */
.v-leave-from{
opacity: 1;
}
.v-leave-active{
transition: opacity 1s ease-in;
}
.v-leave-to {
opacity: 0;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 多个单元素标签之间的切换
// 多个单组件之间的切换
const ComponentA = {
template: '<div>hello world</div>'
}
const ComponentB = {
template: '<div>bye world</div>'
}
const app = Vue.createApp({
data(){
return { show: false }
},
components: {
'component-a': ComponentA,
'component-b': ComponentB
},
methods: {
handleClick(){
this.show = !this.show
}
},
template: `
<div>
<transition mode="out-in" appear>
<component-a v-if="show" />
<component-b v-else="show" />
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
或者也可以写成 动态组件 ,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组件和元素切换动画的实现</title>
<style>
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
}
.v-enter-active{
transition: opacity 1s ease-in;
}
.v-enter-to {
opacity: 1;
}
/* -------------------出场动画----------------------- */
.v-leave-from{
opacity: 1;
}
.v-leave-active{
transition: opacity 1s ease-in;
}
.v-leave-to {
opacity: 0;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 多个单元素标签之间的切换
// 多个单组件之间的切换
const ComponentA = {
template: '<div>hello world</div>'
}
const ComponentB = {
template: '<div>bye world</div>'
}
const app = Vue.createApp({
data(){
return { component: 'component-a' }
},
components: {
'component-a': ComponentA,
'component-b': ComponentB
},
methods: {
handleClick(){
if(this.component === 'component-a'){
this.component = 'component-b'
} else {
this.component = 'component-a'
}
}
},
template: `
<div>
<transition mode="out-in" appear>
<component :is="component" />
</transition>
<button @click="handleClick">切换</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
4.列表动画
本章讲解像 列表 这样的 多元素 如何写 动画效果 ,下面的代码中每次 点击按钮向数组前方添加一个数字,能看到了添加的数字从下到上的一个动画效果 代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>列表动画</title>
<style>
.lsit-item{
display: inline-block;
margin-right: 10px;
}
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
transform: translateY(30px);
}
.v-enter-active{
transition: all .5s ease-in;
}
.v-enter-to {
opacity: 1;
transform: translateY(0);
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 列表动画的实现
const app = Vue.createApp({
data(){
return { list: [1, 2, 3] }
},
methods: {
handleClick(){
this.list.unshift(this.list.length + 1)
}
},
template: `
<div>
<transition-group>
<span class="lsit-item" v-for="item in list" :key="item">{{item}}</span>
</transition-group>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
但是这还是有一点点小瑕疵, 只有加进去的数字有动画效果 ,在这之前 每次添加数字时,右侧的数字只会很生硬的想有挤过去 ,如果想让 列表其他元素挤到右侧时候也有动画 该怎么办呢,使用 .v-move 来解决此问题,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>列表动画</title>
<style>
.lsit-item{
display: inline-block;
margin-right: 10px;
}
/* -------------------入场动画----------------------- */
.v-enter-from{
opacity: 0;
transform: translateY(30px);
}
.v-enter-active{
transition: all .5s ease-in;
}
.v-enter-to {
opacity: 1;
transform: translateY(0);
}
/* 其他列表移动时候可以.v-move做一些描述 */
.v-move{
transition: all .5s ease-in;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 列表动画的实现
const app = Vue.createApp({
data(){
return { list: [1, 2, 3] }
},
methods: {
handleClick(){
this.list.unshift(this.list.length + 1)
}
},
template: `
<div>
<transition-group>
<span class="lsit-item" v-for="item in list" :key="item">{{item}}</span>
</transition-group>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
5.状态动画
需求:假如说 数字 1变成 10 这个过程,就是一个状态的更改,例子如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>状态动画</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 状态动画
const app = Vue.createApp({
data(){
return {
number: 1,
animateNumber: 1
}
},
methods: {
handleClick(){
this.number = 10
if(this.animateNumber < this.number){
const animation = setInterval(() => {
this.animateNumber += 1
if(this.animateNumber === 10){
clearInterval(animation)
}
}, 100);
}
}
},
template: `
<div>
<div>{{animateNumber}}</div>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
五、Vue 中的高级语法
本章中,将会着重介绍 Vue 中 混入, 插件 , 自定义指令 等 扩展语法 ,以及 render函数 ,Teleport传送门 等 高级语法,帮助大家更加深入的了解 Vue 原理以及扩展性原则,让大家能够具备一定的 高级封装能力。
1.Mixin 混入的基础语法
这章讲解一下 vue 中相关的一些 复用性代码 该怎么去写,首先介绍一下 mixin 混入 , mixin 混入: 就是把某一些内容混入到哪里去 。
局部混入
如下代码,定义了一个 myMixin 混入,并且 混入的代码中 同样有一个名称为 number 的 变量 ,
index.htm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 定义mixin 混入
// 组件 data 优先级高于mixin data 优先级
const myMixin = {
data(){
return {
number: 2,
count: 222
}
}
}
const app = Vue.createApp({
data(){
return { number: 1 }
},
mixins: [myMixin], // 2. 引入混入
methods: {
handleClick(){
console.log('handleClick')
}
},
template: `
<div>
<div>{{number}}</div>
<div>{{count}}</div>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
如果 混入的变量与文件中的变量名相同 的情况下, 使用文件自己的 number 变量,如果文件中没有 number 这个变量,就会使用 混入的代码中的 number 变量 ,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 定义mixin 混入
// 组件 data 优先级高于mixin data 优先级
const myMixin = {
data(){
return {
number: 2,
count: 222
}
}
}
const app = Vue.createApp({
data(){
return {
// number: 1
}
},
mixins: [myMixin], // 2. 引入混入
methods: {
handleClick(){
console.log('handleClick')
}
},
template: `
<div>
<div>{{number}}</div>
<div>{{count}}</div>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
生命周期 也是可以 混入 的,那么如果 组件代码中与混入代码中 都有 created 会怎么样呢,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 1. 定义mixin 混入
// 组件 data, methods 优先级高于mixin data, methods 优先级
// 生命周期函数,先执行 mixin 里面的,再执行组件里面的
const myMixin = {
data(){
return {
number: 2,
count: 222
}
},
created(){
console.log('我是混入的created')
},
methods: {
handleClick(){
console.log('mixin handleClick')
}
}
}
const app = Vue.createApp({
data(){
return {
number: 1
}
},
created(){
console.log('我是自己的created')
},
mixins: [myMixin], // 2. 引入混入
methods: {
handleClick(){
console.log('handleClick')
}
},
template: `
<div>
<div>{{number}}</div>
<div>{{count}}</div>
<button @click="handleClick">增加</button>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
生命周期函数 ,会先执行 mixin 里面的,再执行 组件 里面的 。
全局混入
假如有这样一个场景:A组件 中引入了 mixin混入代码 ,并且 A组件 中引入了 B组件 ,在这个时候如果 B组件 想使用 A组件 引入的 mixin混入代码 , 可以直接使用吗? 答案是不可以的 。如果也想使用 mixin混入的代码, 必须也要在 B组件 内引入 mixin混入代码 才能使用,这就跟 vue 的 局部组件 一个道理,那么 如何引入一次混入,任何组件都可以使用 呢,接下来讲解一下 全局混入 的使用方法。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 组件 data, methods 优先级高于mixin data, methods 优先级
// 生命周期函数,先执行 mixin 里面的,再执行组件里面的
const app = Vue.createApp({
data(){
return {
number: 1
}
},
created(){
console.log('我是自己的created')
},
methods: {
handleClick(){
console.log('handleClick')
}
},
template: `
<div>
<div>{{number}}</div>
<child />
<button @click="handleClick">增加</button>
</div>`
})
// 创建全局mixin
app.mixin({
data(){
return {
number: 2,
count: 222
}
},
created(){
console.log('我是混入的created')
},
methods: {
handleClick(){
console.log('mixin handleClick')
}
}
})
// 创建子组件
app.component('child', {
template: "<div>{{count}}</div>"
})
const vm = app.mount('#root')
</script>
</html>
自定义属性与mixin
下面代码中的 number 属性,这种在 实例里 或者 组件里 面定义的内容,实际上是一个 自定义属性 ,既不在 data 里,也不在 methods 里,又不在 计算属性 里,又不在 各种各样(例如:watch等等...) 的方法里,直接 定义在组件的最外层这样的一个属性 ,这种内容,我们把它叫做 自定义属性 。
this.options 里面来,直接 this.number 是获取不到的,用 this.$options.number 这种方式才可以获取到。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 组件 data, methods 优先级高于mixin data, methods 优先级
// 生命周期函数,先执行 mixin 里面的,再执行组件里面的
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 2,
template: `
<div>
<div>{{this.$options.number}}</div>
</div>`
})
const vm = app.mount('#root')
</script>
</html>
组件的 自定义属性 优先级高于 mixin 自定义属性 优先级
自定义属性优先级修改
虽然上面说到了 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级 ,但是vue 是 支持修改自定义属性优先级 的,代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mixin 混入的基础语法</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 组件 data, methods 优先级高于mixin data, methods 优先级
// 生命周期函数,先执行 mixin 里面的,再执行组件里面的
const myMixin = {
number: 1
}
const app = Vue.createApp({
mixins: [myMixin],
number: 2,
template: `
<div>
<div>{{this.$options.number}}</div>
</div>`
})
// 对自定义属性number的合并策略做修改,
app.config.optionMergeStrategies.number = (mixinVal, appVal) => {
return mixinVal || appVal // mixinVal 有就用 mixinVal,否则用appVal
}
const vm = app.mount('#root')
</script>
</html>
总结
- 组件 的 data, methods 优先级高于mixin data, methods 优先级
- 生命周期函数,先执行 mixin 里面的,再执行组件里面的
- 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级
2.开发实现 Vue 中的自定义指令
在这之前想 给input默认获取到焦点 ,我们都需要通过 ref 的方式来 触发焦点 ,如下代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
mounted(){
this.$refs.input.focus()
},
template: `
<div>
<input ref="input" />
</div>
`
})
const vm = app.mount('#root')
</script>
</html>
这么写代码, 自定聚焦的逻辑无被复用,像 对dom 的操作 ,其实我们可以通过 封装自定义指令 ,来实现 dom逻辑 的 复用 。
(全局自定义指令)实现input默认触发焦点
下面的 v-focus 就是我们 自定义的指令 ,自定义指令 有一个 生命周期函数 mounted ,指的是 当我这个指令挂载到某一个 dom 元素上的时候 , mounted函数会自动执行 ,执行之后会 接收第一个参数 el , el 就是绑定指令的元素 , 挂载dom 之后,执行 el.focus() 就实现自己聚焦的效果。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
template: `
<div>
<input v-focus />
</div>
`
})
// 定义全局自定义指令
app.directive('focus', {
mounted(el){ // 接收el元素
el.focus()
}
})
const vm = app.mount('#root')
</script>
</html>
那么除了 mounted 生命周期函数 还有哪些其它的 生命周期函数 呢,具体如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
data(){
return {
hello: true
}
},
template: `
<div>
<div v-show="hello">
<input v-focus />
</div>
</div>
`
})
// 定义全局自定义指令
app.directive('focus', {
beforeMount(){ // 指令即将挂载到页面元素上的时候
console.log('beforeMount')
},
// 接收el元素
mounted(el){ // 指令挂载到页面元素时
console.log('mounted')
el.focus()
},
beforeUpdate(){ // (v-show的值变更)重新渲染input之前时
console.log('beforeUpdate')
},
updated(){ // (v-show的值变更)重新渲染input时
console.log('updated')
},
beforeUnmount(){ // (v-if=false)被销毁之前
console.log('beforeUnmount')
},
unmounted(){ // (v-if=false)被销毁之后
console.log('unmounted')
}
})
const vm = app.mount('#root')
</script>
</html>
(局部自定义指令)实现input默认触发焦点
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 局部自定义指令
const directives = {
focus: {
mounted(el){ // 接收el元素
el.focus()
}
}
}
const app = Vue.createApp({
// directives: directives , // 等同于下面的写法
directives, // 引入局部自定义指令
template: `
<div>
<input v-focus />
</div>
`
})
const vm = app.mount('#root')
</script>
</html>
实战案例
用 自定义指令来决定header距离顶部的位置 ,我们通过 传入data 中的 top值 ,就可以实现 动态更改header距离顶部的距离 ,代码如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<style>
.header{
position: absolute;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
data(){
return {
top: 500
}
},
template: `
<div>
<div v-pos="top" class="header">
<input />
</div>
</div>
`
})
// 定义全局自定义指令
app.directive('pos', {
/**
* @param {object} el - 绑定指令的dom元素
* @param {object} binding - 自定义指令捆绑元素身上的信息
*/
mounted(el, binding){
el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
}
})
const vm = app.mount('#root')
</script>
</html>
但是这里有个问题,我们在 浏览器 通过 vm.top = 1000 修改 top 的位置,会发现 header 位置 ,并没有改变 ,这是 因为数据发生变化时,自定义指令中的 mounted 并不会重新执行 ,想要让位置改变需要使用 updated 生命周期函数 ,代码如下:
更新数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<style>
.header{
position: absolute;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
data(){
return {
top: 500
}
},
template: `
<div>
<div v-pos="top" class="header">
<input />
</div>
</div>
`
})
// 定义全局自定义指令
app.directive('pos', {
/**
* 初始化时
* @param {object} el - 绑定指令的dom元素
* @param {object} binding - 自定义指令捆绑元素身上的信息
*/
mounted(el, binding){
el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
},
/**
* 数据更新时
* @param {object} el - 绑定指令的dom元素
* @param {object} binding - 自定义指令捆绑元素身上的信息
*/
updated(el, binding){
el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
}
})
const vm = app.mount('#root')
</script>
</html>
定义指令时逻辑简写
在 vue 中,如果你的 自定义指令 里只有 mounted 与 updated , 而且它俩逻辑写的同样情况下,可以把它简写 , 简写的形式 directive 的第二个参数是一个箭头函数 ,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<style>
.header{
position: absolute;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
data(){
return {
top: 500
}
},
template: `
<div>
<div v-pos="top" class="header">
<input />
</div>
</div>
`
})
// --------------------简写形式----------------------------------------------------
/**
* 初始化时
* @param {object} el - 绑定指令的dom元素
* @param {object} binding - 自定义指令捆绑元素身上的信息
*/
app.directive('pos', (el, binding) => {
el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
})
// --------------------非简写形式----------------------------------------------------
// // 定义全局自定义指令
// app.directive('pos', {
// /**
// * 初始化时
// * @param {object} el - 绑定指令的dom元素
// * @param {object} binding - 自定义指令捆绑元素身上的信息
// */
// mounted(el, binding){
// el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
// },
// /**
// * 数据更新时
// * @param {object} el - 绑定指令的dom元素
// * @param {object} binding - 自定义指令捆绑元素身上的信息
// */
// updated(el, binding){
// el.style.top = `${ binding.value }px` // 自定义指令传入过来的值
// }
// })
const vm = app.mount('#root')
</script>
</html>
模仿v-on:click的写法
在上面我们使用了自己定义的 自定义指令 v-pos ,那我们回想一下在 vue 中, 有 v-on:click ,那我们也可以这样写:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发实现 Vue 中的自定义指令</title>
<style>
.header{
position: absolute;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// 自定义指令 directive
const app = Vue.createApp({
data(){
return {
distance: 500, // 距离
direction: 'left' // 方向
}
},
template: `
<div>
<div v-pos:[direction]="distance" class="header">
<input />
</div>
</div>
`
})
/**
* 初始化时
* @param {object} el - 绑定指令的dom元素
* @param {object} binding - 自定义指令捆绑元素身上的信息
*/
app.directive('pos', (el, binding) => {
el.style[binding.arg] = `${ binding.value }px` // 自定义指令传入过来的值
})
const vm = app.mount('#root')
</script>
</html>
3.Teleport 传送门功能
这章我们讲解一下 vue3 提供给我们的 Teleport 传送门 ,我们看一看 传送门 是什么,我们该 如何使用它 呢?
需求:假如我们需要写一个 点击按钮弹出一个黑色半透明弹窗,遮盖整个屏幕 ,我们的 布局代码以及样式 是这样写的:
点击前
点击后
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teleport 传送门功能</title>
<style>
.area{
position: absolute;
left: 50%;
top: 50%;
/* transform: translateX(-50%);
transform: translateY(-50%); */
/* 上面这样写下面的会把上面的样式覆盖掉,所以要用简写的形式*/
transform: translateY(-50%, -50%);
width: 200px;
height: 300px;
background: green;
}
.mask{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
opacity: .5;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// Teleport 传送门
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleBtnClick(){
this.show = !this.show
}
},
template: `
<div class="area">
<button @click="handleBtnClick">按钮</button>
<div class="mask" v-show="show"></div>
</div>
`
})
const vm = app.mount('#root')
</script>
</html>
你会发现 蒙层大小并不是我们想要的全屏的大小 ,这是因为 黑色蒙层是绝对定位的是自己的父级 area 这个元素,而 area 这个元素的大小只有200 * 300 的大小 ,如果 我们想实现黑色遮罩层全屏,通常需要把这个遮罩层放入到 body 内,从而变成 body 直接的子级,这样绝对定位的就是body ,这样写就会 非常非常的麻烦 ,而 vue3 给我们提供了一个 teleport标签(传送门) ,我们只需要 用teleport标签 将黑色弹层包裹,并且写上 to 属性, to 属性 内写入我们要传入到的标签名称,就可以将弹窗传入到想要传入的标签内 ,
传送门功能实现传送弹窗功能
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teleport 传送门功能</title>
<style>
.area{
position: absolute;
left: 50%;
top: 50%;
/* transform: translateX(-50%);
transform: translateY(-50%); */
/* 上面这样写下面的会把上面的样式覆盖掉,所以要用简写的形式*/
transform: translateY(-50%, -50%);
width: 200px;
height: 300px;
background: green;
}
.mask{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
opacity: .5;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// Teleport 传送门
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleBtnClick(){
this.show = !this.show
}
},
template: `
<div class="area">
<button @click="handleBtnClick">按钮</button>
<teleport to="body">
<div class="mask" v-show="show"></div>
</teleport>
</div>
`
})
const vm = app.mount('#root')
</script>
</html>
这样就实现了我们想要的效果, 全屏的遮盖效果 , 然而我们也可以 根据 id 传送到指定的标签内 ,下面的案例中就是 将弹窗盒子传入了 id 为 hello 的元素中,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teleport 传送门功能</title>
<style>
.area{
position: absolute;
left: 50%;
top: 50%;
/* transform: translateX(-50%);
transform: translateY(-50%); */
/* 上面这样写下面的会把上面的样式覆盖掉,所以要用简写的形式*/
transform: translateY(-50%, -50%);
width: 200px;
height: 300px;
background: green;
}
.mask{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
opacity: .5;
}
</style>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
<div id="hello"></div>
</body>
<script>
// Teleport 传送门
const app = Vue.createApp({
data(){
return {
show: false
}
},
methods: {
handleBtnClick(){
this.show = !this.show
}
},
template: `
<div class="area">
<button @click="handleBtnClick">按钮</button>
<teleport to="#hello">
<div class="mask" v-show="show"></div>
</teleport>
</div>
`
})
const vm = app.mount('#root')
</script>
</html>
编译后的代码如下, 可以看到遮罩层被放入到 id 为 hello 的盒子中
总结
我们可以 封装组件 时,有弹窗的需求,可以用 teleport 用传送门功能 。
4.更加底层的 render 函数
本章讲解一下 render 函数 ,它是 vue 中 比较底层 的一块内容,来简单的了解一下。
我们封装了一个 title 组件,我们的 title 组件 中有一个 h1 标签,但是这个 h几的标签 ,我们想 通过外部传递的参数来控制 ,那我们肯定第一印象是 通过 props 传入一个值来进行判断来显示对应的h几的标签 ,我们通过 template 以及 render 函数 来实现此效果,代码如下:
template 实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>更加底层的 render 函数</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// render function
// template -> render -> h -> 虚拟DOM(JS 对象) -> 真实DOM -> 展示到页面上
const app = Vue.createApp({
template: `
<my-title :level="2">
hello
</my-title >
`
})
app.component('my-title', {
props: ['level'],
template: `
<h1 v-if="level === 1"><slot /></h1>
<h2 v-if="level === 2"><slot /></h2>
<h3 v-if="level === 3"><slot /></h3>
<h4 v-if="level === 4"><slot /></h4>
<h5 v-if="level === 5"><slot /></h5>
<h6 v-if="level === 6"><slot /></h6>
`
})
const vm = app.mount('#root')
</script>
</html>
render 函数实现
通过 render 函数 , 就可以实现非常多的代码实现的功能,而且很简单, render 函数 与 template 之间的关系是什么呢,实际上 template 在 底层被编译之后 ,会 生成这个 render函数,这个 render 函数 它里面调用 vue 的 h 方法 去 返回一个内容 ,这个内容实际上是 vue 中一个叫做 虚拟dom 的一个东西,虚拟dom是什么呢,它实际上是 dom 节点的一个 js对象 的表述
// 真实的dom的样子
<div>hello</div>
// 虚拟dom的样子(虚拟dom就是js对象,它是对真实dom对象的一个映射)
{
tagName: 'div',
text: hello,
attributes: {}
}
上面的 真实的 div 会被映射成下面这个 虚拟dom , vue 里面把 template 变成 render 函数 , render 函数 再去返回一个 虚拟 dom ,它的意义在于什么呢,它有 2个好处 :
- 它可以让vue的性能更快
- 它可以让vue具备一个跨平台的能力,不仅可以写网页上的东西,还可以通过weex这样的开发工具,去编写移动端的一些代码 ,所以说 虚拟 dom 它的作用还是很大的,但是它是一个 比较深的内容 ,如果大家感兴趣可以查阅相关内容。
h 函数 就是返回 虚拟dom 节点 的一个 函数, 第一个参数是告诉虚拟 dom的标签是什么,第二个参数是标签的属性,第三个参数是标签的内容 。
上面说了一些 虚拟dom 的内容,下面看一下使用 render 函数 实现的效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>更加底层的 render 函数</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// render function
// template -> render -> h -> 虚拟DOM(JS 对象) -> 真实DOM -> 展示到页面上
const app = Vue.createApp({
template: `
<my-title :level="5">
hello dell
</my-title >
`
})
app.component('my-title', {
props: ['level'],
// 用render实现的代码
render(){
// 引入h函数
const { h } = Vue;
// 参数1:标签名字
// 参数2:属性
// 参数3:内容
// 在render函数里面或者在vue的其他函数里面,
// 通过this.$slot能获取到slot的相关内容,
// 如果调用默认插槽中的内容可以通过this.$slots.default()来获取
return h('h' + this.level, {}, this.$slots.default())
}
// 原来的代码
// template: `
// <h1 v-if="level === 1"><slot /></h1>
// <h2 v-if="level === 2"><slot /></h2>
// <h3 v-if="level === 3"><slot /></h3>
// <h4 v-if="level === 4"><slot /></h4>
// <h5 v-if="level === 5"><slot /></h5>
// <h6 v-if="level === 6"><slot /></h6>
// `
})
const vm = app.mount('#root')
</script>
</html>
render 函数嵌套
h函数 可以 深层次嵌套 ,这样就可以生成自己想要的结构。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>更加底层的 render 函数</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// render function
// template -> render -> h -> 虚拟DOM(JS 对象) -> 真实DOM -> 展示到页面上
const app = Vue.createApp({
template: `
<my-title :level="1">
hello dell
</my-title >
`
})
app.component('my-title', {
props: ['level'],
render(){
// 引入h函数
const { h } = Vue;
// 参数1:标签名字
// 参数2:属性
// 参数3:内容
// 在render函数里面或者在vue的其他函数里面,
// 通过this.$slot能获取到slot的相关内容,
// 如果调用默认插槽中的内容可以通过this.$slots.default()来获取
return h('h' + this.level, {}, [
this.$slots.default(),
h('h4', {}, 'dell')
])
}
})
const vm = app.mount('#root')
</script>
</html>
5.插件的定义和使用
本章讲解一下 vue 里 插件的概念 ,之前学习过 mixin 混入 ,通过 mixin 混入可以对一些代码逻辑进行一些封装 ,实际上呢,我们如果使用 plugin插件 的这种功能,可以 对代码做更好的封装,那么一起学习一下在vue 里如果做 插件 。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件的定义和使用</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
// plugin 插件, 也是把通用性的功能封装起来
const myPlugin = {
/**
* 插件运行时走的逻辑,我们可以取到vue实例可以根据实例对象进行一些功能的扩展
* @param {object} app - vue实例
* @param {object} options - vue实例
*/
install(app, options){
// 全局的provide name变量,用的地方只需要inject: ['name'],就可以取到
app.provide('name', 'Dell Lee')
// 全局自定义指令
app.directive('focus', {
mounted(el){
el.focus()
}
})
// 全局混入
app.mixin({
mounted(){
console.log('mixin')
}
})
// vue全局属性扩展 app.config.globalProperties
// 扩展$sayHello这个变量,如果加$证明是对vue底层做一个扩展
app.config.globalProperties.$sayHello = 'hello world'
}
}
const app = Vue.createApp({
template: `
<my-title />
`
})
app.component('my-title', {
inject: ['name'],
mounted(){
console.log(this.$sayHello)
},
template: `<div>{{ name }}<input v-focus /></div>`
})
/**
* 注册插件
* @param {object} 参数1 - 插件对象
* @param {object} 参数2 - 给插件传入的信息
*/
app.use(myPlugin, { name: 'dell' })
const vm = app.mount('#root')
</script>
</html>
vuex 以及 ** vue router** 都是通过 plugin 来进行扩展的。
6.数据校验插件开发实例
我希望对表单的数据做一个校验,下面通过 2种方式进行校验
mixin校验数据
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据校验插件开发实例</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
data(){
return { name: 'dell', age: 28 }
},
rules: {
age: {
validate: age => age > 25,
message: '太年轻了'
},
name: {
validate: name => name.length >= 4,
message: 'name长度必须大于等于4'
}
},
template: `
<div>{{name}}, age: {{age}}</div>
`
})
// 对数据做校验的插件
app.mixin({
created(){ // 初始化
for(let key in this.$options.rules){
const item = this.$options.rules[key]
// age改变时,对实例的内容做监控
this.$watch(key, (value) => {
const result = item.validate(value)
if(!result) console.log(item.message)
})
}
}
})
const vm = app.mount('#root')
</script>
</html>
plugin校验数据
单独写一个 mixin可读性差 ,如果放入到 plugin中可读性强 ,建议使用 plugin 。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据校验插件开发实例</title>
<!-- 通过cdn方式引入vue -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
data(){
return { name: 'dell', age: 28 }
},
rules: {
age: {
validate: age => age > 25,
message: '太年轻了'
},
name: {
validate: name => name.length >= 4,
message: 'name长度必须大于等于4'
}
},
template: `
<div>{{name}}, age: {{age}}</div>
`
})
// const validatorPlugin = {
// install(){
// }
// }
// 之前用install中写逻辑,其实也可以像下面这样,写成箭头函数的形式
// 定义插件
const validatorPlugin = (app, options) => {
// 对数据做校验的插件
app.mixin({
created(){ // 初始化
for(let key in this.$options.rules){
const item = this.$options.rules[key]
// age改变时,对实例的内容做监控
this.$watch(key, (value) => {
const result = item.validate(value)
if(!result) console.log(item.message)
})
}
}
})
}
// 注册插件
app.use(validatorPlugin)
const vm = app.mount('#root')
</script>
</html>