在vue中数据绑定存在两种形式,一种是数据单向绑定,另一种是数据双向绑定。其中数据单向绑定可以使用插值表达式 {{ … }} 和 v-bind 这两种形式,而数据双向绑定可以使用 v-model 示例如下:
// 1、数据单向绑定
// 插值表达式
<span>Message: {{ msg }}</span>
// v-bind 单向绑定
<span v-bind:class="back">Message</span>
// 可以简写成 :
<span :class="back">Message</span>
// 2、数据双向绑定
<input v-model="message">
总结:数据单向绑定适用于数据渲染型场景,即根据绑定的数据进行渲染。而数据双向绑定一般适用于表单、搜索框等具有页面输入性质的场景。
在vue中条件渲染涉及到的指令有 v-if 、v-else、v-else-if、v-show 这四个。其中前三个一般可以组合使用,而 v-show 和其他三个性质不同,是单独使用。示例如下:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
<h1 v-show="isTrue">Hello!</h1>
通过上面的示例,我们可以看出,条件渲染都是通过判断指定的数据是否满足条件来进行渲染判断,例如上面的 type 和 isTrue 如果条件为 true 则渲染,反之则不渲染。此外,v-if 是基于DOM元素的销毁和创建来实现显示和隐藏的,对性能消耗较高。而 v-show 则是通过对 DOM 元素的 display 属性进行动态修改来实现元素的显示和隐藏,对性能影响小。因此,如果是频繁的切换显示和隐藏,使用 v-show 较好,反之则使用 v-if
在vue中列表渲染使用的指令是 v-for ,使用示例如下:
// 普通用法
<li v-for="item in items">
{{ item.message }}
</li>
// 带元素下表用法
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
// 通过 key 管理状态用法,推荐使用
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
除了列表渲染之外,我们对列表数据最常用的还有列表数据进行变更操作:常见的有如下操作:
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return numbers.value.filter((n) => n % 2 === 0)
})
具体可参考官网:v-for指令详解
在vue中事件指的是用户交互或系统触发的操作,例如,点击按钮、刷新页面、滚动页面等等。对于这些事件,我们需要监听处理。因此,事件处理是一个非常重要的环节。事件处理可分为事件监听和处理实现两个部分,事件监听可以使用指令 v-on 或者其简写 @ 来实现。通过 @ 可以绑定一个事件处理函数,用来处理事件。具体示例如下:
// 不带参数
<span @click="handleTest"></span>
const handleTest = () => {
console.log('hello world')
}
// 带参数
<span @click="handleTest('hello world')"></span>
const handleTest = (param: string) => {
console.log(param)
}
从上面的示例中,我们了解了事件处理的方式,但这还不够,我们必须对事件本身有足够的了解,在vue中存在多种事件,不同事件对应不同的操作。常用的事件有如下:
1、click 点击事件
2、submit 表单提交事件
3、元素获得焦点事件
4、keydown 键盘按下事件
5、keyup 键盘松开事件
除了以上的常用事件之外,还有另一个事件修饰符也同样重要,通过事件修饰符,我们可以有效地控制事件的行为。常见的时间修饰符有:
通过事件修饰符,我们可以了解到每个DOM元素都存在默认的行为,例如 a 标签的默认行为链接跳转,表单元素提交的默认行为是刷新页面。这些DOM元素的默认行为都可以通过事件修饰符来控制。
watch 监听器本身是一个 API 函数,所有的函数在使用时都是以 函数名 + ()的形式组成。例如:watch()、useRoute()等等。在 setup 组合式API中,我们可以使用 watch 监听器来实现对某个响应式数据动态变化的感知处理。只要目标数据发生改变,watch 监听器就能够实时的捕捉到并做出相应处理。watch 监听器的默认基本结构如下:
watch(data, (newData, oldData) => {
// 函数处理内容
})
从 watch 监听器的基本结构可以看出,watch 监听器内部由两部分组成:
1、监听目标对象
2、回调处理函数
对于监听目标有硬性要求:
1、必须是响应式对象
2、不能直接监听响应式对象内的属性值,可间接监听,使用 getter 函数实现
示例如下:
const x = ref(0)
const y = ref(0)
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
const obj = reactive({ count: 0 })
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
对于回调处理函数,我们可以通过添加参数来观察监听对象变化前后的内容,回调处理函数的第一个参数默认是变化后数据,第二个参数是变化前数据。这些参数我们可加也可不加,这取决于我们是否要观察监听对象。
此外,由于 watch 监听器是被动触发的,它是在感知到监听对象数据变化时才执行的,因此,可以说默认的 watch 监听器是懒执行的。但在某些场景下,我们希望它能够在当前组件初始化时就主动执行一次该怎么办呢?我们可以通过在 watch 监听器中传入 immediate:true 选项来实现。示例如下:
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
每一个vue组件实例在创建时都会经历一系列的初始化步骤,这些初始化步骤共同构成了组件生命周期。下面是比较重要的组件生命周期阶段:
1、创建阶段
涉及两个过程 beforeCreate 组件创建之前、created 组件创建之后
2、挂载阶段
涉及两个过程 beforeMount 组件数据挂载渲染之前、mounted 组件数据挂载渲染之后
3、更新阶段
涉及两个过程 beforeUpdate 组件数据更新之前、updated 组件数据更新之后
4、销毁阶段
涉及两个过程 beforeUnmount 组件销毁之前、unmounted 组件销毁之后
一般来说,我们使用最多的就是 mounted 和 updated 这两个过程,可以重点掌握,示例如下:
onMounted(() => {
console.log('页面挂载完成,触发了onMounted钩子函数');
timer = setInterval(() => {
console.log('定时器正在运行中', count++)
}, 1000)
})
onUpdated(() => {
console.log('数据发生了更新,触发了onUpdated钩子函数')
})
在vue中响应式是一个核心点,当我们使用vue时,必定会涉及响应式数据,我们无时不刻都在和响应式数据打交道,那么,我们该如何创建或者说定义响应式数据呢?vue为我们提供了多种创建响应式数据的 API 函数,其中最常用的有如下两种:
1、ref() 函数
ref 函数的特点是适用于所有类型的响应式数据创建,它既可以创建基本类型的响应式数据,也可以创建复杂类型的响应式数据。此外,另一个特点是,通过 ref 创建的响应式数据对象是一个包装对象,真正的源数据需要通过 **.value **来获取。示例如下:
import { ref } from 'vue'
const count = ref(0)
count.value++
const objectRef = ref({ count: 0 })
// 这是响应式的替换
objectRef.value = { count: 1 }
因此,想要获取 ref 函数括号中的数据,需要使用 .value
2、reactive() 函数
reactive 函数不可以用来创建基本数据类型的响应式数据,只能创建对象和集合类型的响应式数据。并且不用像 ref 那样,需要使用 **.value **来获取源数据,可以直接使用。示例如下:
const raw = { count: 0 }
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
proxy.count++
计算属性的作用是将复杂的处理逻辑封装起来,然后以响应式的方式提供给模板使用,避免直接在模板中处理逻辑。当计算的源数据发生变化时,计算属性会重新计算处理并给出最新的处理结果,因此,计算属性是响应式的。使用示例如下:
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
从计算属性 computed 的函数内容可以看出,其内使用了 getter 函数,那么什么是 getter 函数呢?
在vue中,具有返回数据的函数函数都可以称 getter 函数。 同样,所有设置数据的函数都可以称 setter 函数。示例如下:
const obj = ref({
count: 0
})
// getter 函数中的 return 可以省略,并且可以带参数
const getter1 = () => return obj.count
const getter2 = () => obj.count
const getter3 = (param:number) => obj.count + number
// setter 函数也可以带参数
const setter1 = () => obj.count = 100
const setter2 = (param:number) => obj.count = number
模板元素指的是 template 标签域中的所有元素,一般来说,这些元素是一些原生DOM元素、UI框架封装的组件、自定义组件。因此,当我们需要获取这些元素并做一些使用时,我们该怎么办呢?在原生的 JS 中,获取DOM元素一般是如下方式:
<div id="example">Hello, world!</div>
const elem = document.getElementById('example')
<div class="item">Hello, world!</div>
const elems = document.getElementsByClassName('item')
主要常用的是通过 DOM 元素上的一些标识,常用的有类选择器、id选择器这两种。但在vue框架下,它为我们提供了一种更加方便的获取元素的方式,就是通过 ref 元素属性来获取,其实也是一种为模板元素设置选择标识的一种方式。使用方式如下:
<template>
<input ref="input" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
此外,需要特别注意的一点是,当我们获取模板元素并试图去使用的时候,我们必须选择合适的组件生命阶段,我们只能在组件挂载渲染完成之后的阶段使用获取的模板元素,这是因为在当前组件都还没有渲染完成时,其内的元素都还不存在呢!因此,在 onMounted、onUpdated等阶段,我们可以放心使用。
在vue语言框架下,任何的** .vue** 单文件都可以是组件,每个组件都是独立存在的,整个应用的构建就像搭积木一样,每一个组件都将是构成整个应用的元素。
除了了解单文件组件之外,另一个经常使用到的是:动态组件,动态组件指的是可以在不同的组件之间来回切换,示例如下:
<template>
<div>
<button @click="toggleComponent">Toggle Component</button>
<component :is="currentComponent"></component>
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue'
<script setup>
const currentComponent = ref('ComponentA')
const toggleComponent = () => {
currentComponent.value =
currentComponent.value === 'ComponentA' ? 'ComponentB' : 'ComponentA'
}
const ComponentA = defineAsyncComponent(() =>
import(/* webpackChunkName: "ComponentA" */ './ComponentA.vue')
)
const ComponentB = defineAsyncComponent(() =>
import(/* webpackChunkName: "ComponentB" */ './ComponentB.vue')
)
return {
currentComponent,
toggleComponent,
ComponentA,
ComponentB,
}
</script>
在上面的实例中,传递给 :is 的值有两种情况,
1、全局注册的组件名
2、当前文件引入的组件对象
在vue3中组件注册一共有两种方式:
1、全局注册:
全局注册指的是在主应用配置文件中进行的注册,其是在 main.ts 中完成的。
// 先引入,后注册
import ComponentA from './App.vue'
app.component('ComponentA', ComponentA)
像这样全局注册的组件,可以在此应用的任意其他组件模板域中使用。使用如下:
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
// 或者如下
<component-a></component-a>
虽然全局注册能够使组件可以在应用任意其他组件模板域中使用,但其缺点也很明显:主要有以下两点:
(1)可能存在无用或者不必要的注册,这些注册了但没有被使用的组件是无法自动删除的。导致无用代码增多。
(2)使用全局注册的组件将导致组件之间的依赖关系变得不那么明确,不能够直观的看出引入组件的位置,使得代码的维护性变差。因为,一旦我们需要修改引入的组件,还需要去 main.ts 中查找其具体位置。
2、局部注册:
局部注册指的是直接在需要引入使用的组件中显示 import 导入,如果我们使用的是 setup 组合式API,那么就可以直接在该组件的模板域中使用,而不需要再像选项式API中那样显示的注册引入的组件。示例如下:
// template 模板域中
<template>
<component-a></component-a>
</template>
// setup 组合式API中
<script setup lang="ts">
import ComponentA from './App.vue'
</script>
相比于全局注册,局部注册使得组件之间的依赖关系更加清晰,使用起来也很方便,推荐使用。
总结:对于那些全剧共用的组件,例如,components 文件夹下的组件,建议使用全局注册,其他的则采用局部注册。
**(1)使用 props **
在 setup 组合式API中,我们可以在单文件组件中显式使用宏命令 defineProps 声明 props ,这样Vue才能够知道外部传入的哪些是 props 。使用方式如下:
// MenuItem 单文件组件中,源组件中定义
const props = defineProps({
data: {
type: Object,
required: true,
},
});
// Menu 单文件组件中,引入方组件绑定传值
<menu-item :data="item"></menu-item>
在上面的 defineProps 定义中,只定义了一个绑定属性 data 并对该属性进行了一些约束,例如,数据类型约束、是否为必须绑定属性约束等等。因此,当我们使用 MenuItem 源组件时,就必须强制性满足这些约束条件。当然,我们也可以同时定义多个绑定属性,例如下方示例:
// MenuItem 单文件组件中,源组件中定义
const props = defineProps({
data: {
type: Object,
required: true,
},
value: Integer
});
// Menu 单文件组件中,引入方组件绑定传值,必传 data
<menu-item :data="item"></menu-item>
// value 属性为可选绑定属性,可绑亦可不绑
<menu-item :data="item" :value="2023"></menu-item>
在上面的示例中,我们定义了两个组件绑定属性 data 和 value 其中 data 属性为必选绑定属性,只要其他组件引入使用该源组件,就必须绑定 data 属性。对于哪些没有显示声明 **required: true **的属性,都默认为可选绑定属性。
此外,我们约定如下:任何对象类型的数据即用大括号包裹的数据,其中的所有属性的类型均使用包装类型,不要使用基本数据类型。示例如下:
// 可以这样使用,都是用包装类型
export type UserInfo = {
id: String;
name: String;
age: Integer;
avatar: String;
}
// 不建议如下使用
export type UserInfo = {
id: string;
name: string;
age: int;
avatar: string;
}
(2)使用 pinia
pinia是vue的存储库,它允许你跨组件实现共享状态,这个状态就是数据。因此,我们也可以借助 pinia 来实现组件通信。其在 setup 组合式API中的使用方式如下:
// 从pinia中引入defineStore函数来定义store
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
import {defineStore} from 'pinia'
// 一个 Store 就是一个实体,类比Java中的实体类
// 头部区域状态管理
export const useHeader = defineStore('useHeader', {
// 类比Java中实体类的成员属性
state: () => {
return {
collapse: false
}
},
// 类比Java中的 Getter 方法。推荐使用箭头函数,依赖state
getters: {
getCollapse: (state) => {
return state.collapse
}
},
// 类比Java中的 Setter 方法
actions: {
setCollapse() {
this.collapse = !this.collapse
}
}
})
const headerStore = useHeader()
const { collapse } = storeToRefs(headerStore)
插件是一种可复用的功能模块,可以为Vue应用程序提供额外的功能或添加全局级别的组件或指令。
如何创建自定义插件呢?下面是一个用于将公共组件注册成全局组件的插件。我们单独使用一个插件来完成这项功能,从而避免将注册过程全部写在 main.ts 程序入口文件中。这样使得我们可以像使用 pinia、vue-router等插件一样。
// 自定义插件
import SvgIcon from "@/components/SvgIcon/SvgIcon.vue";
import SimplePage from "@/components/Pagination/SimplePage.vue";
import SimpleUpload from "@/components/Upload/SimpleUpload.vue";
const components = {SvgIcon, SimplePage, SimpleUpload}
export default {
// 必须是install方法
install(app: any) {
// 注册方式一
app
.component('SvgIcon', SvgIcon)
.component('SimplePage', SimplePage)
.component('SimpleUpload', SimpleUpload)
// 注册方式二
for (const [key, component] of Object.entries(components)) {
app.component(key, component);
}
}
}
import globalPlugin from "@/components/index"
const app = createApp(App);
// 注册插件
app.use(router);
app.use(pinia);
app.use(globalPlugin);
app.mount("#app");
一个插件最重要的组成就是 install 函数,只要当前插件被注册,那么,当应用启动时,install 方法会自动执行一次,已完成一些初始化功能。因此,创建自定义插件的关键就在于如何实现 install 函数中的内容。
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
在组件基础章节中,我们已经介绍了通过特殊的 元素来实现动态组件的用法:
<component :is="activeComponent" />
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态,当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。如果我们希望在组件切换显示时,依旧能够保留 切走 时的状态,那么,我们可以用 内置组件将这些动态组件包装起来:
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
如果我们想为一个元素添加动画效果,我们可以使用 vue 提供的内置组件 Transition,我们只需要将需要挂载动画的元素插入到 Transition 组件中即可。
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
以下是最基本用法的示例:
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
/* 下面我们会解释这些 class 是做什么的 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
基于CSS的过渡动画
我们只需要关注,动画开始和结束两个状态,以及两个进入和离开两个过程。分别如下:
// 动画过程
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
// 前后状态
.v-enter-from,
.v-leave-to {
opacity: 0;
}
为过渡动画取名
我们可以给 组件传一个 name prop 来声明一个过渡效果名:
<Transition name="fade">
...
</Transition>
对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。比如,上方例子中被应用的 class 将会是 fade-enter-active 而不是 v-enter-active。这个“fade”过渡的 class 应该是这样:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
配合 CSS 的 transition 使用
一般都会搭配原生 CSS 过渡一起使用,正如你在上面的例子中所看到的那样。这个 transition CSS 属性是一个简写形式,使我们可以一次定义一个过渡的各个方面,包括需要执行动画的属性、持续时间和速度曲线。
下面是一个更高级的例子,它使用了不同的持续时间和速度曲线来过渡多个属性:
<Transition name="slide-fade">
<p v-if="show">hello</p>
</Transition>
/*
进入和离开动画可以使用不同
持续时间和速度曲线。
*/
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
配合 CSS 的 animation 使用
原生 CSS 动画和 CSS transition 的应用方式基本上是相同的,只有一点不同,那就是 *-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除。
对于大多数的 CSS 动画,我们可以简单地在 *-enter-active 和 *-leave-active class 下声明它们。下面是一个示例:
<Transition name="bounce">
<p v-if="show" style="text-align: center;">
Hello here is some bouncy text!
</p>
</Transition>
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
自定义过渡 class
你也可以向 传递以下的 props 来指定自定义的过渡 class:
你传入的这些 class 会覆盖相应阶段的默认 class 名。这个功能在你想要在 Vue 的动画机制下集成其他的第三方 CSS 动画库时非常有用,比如 Animate.css:
<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
name="custom-classes"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show">hello</p>
</Transition>
此外,关于在 Vue 3 的 transition 组件中,过渡动画分为几个阶段,以下是一个旋转动画的开始和结束过程的分析:
类似地,在元素从 DOM 中移除时,Vue 会自动应用一系列 CSS 类名,例如 rotate-leave-from、rotate-leave-active 和 rotate-leave-to。这些类名用于定义元素移除时的旋转动画。
以下是一个简单的 Vue 3 旋转动画示例:
<template>
<div>
<button @click="show = !show">Togglebutton>
<transition name="rotate">
<div v-if="show">I am a rotating element!div>
transition>
div>
template>
<script>
import { ref } from "vue";
export default {
setup() {
const show = ref(true);
return { show };
},
};
script>
<style>
.rotate-enter-from,
.rotate-leave-to {
transform: rotate(0deg);
}
.rotate-enter-active,
.rotate-leave-active {
transition: transform 1s ease;
}
.rotate-enter-to,
.rotate-leave-from {
transform: rotate(360deg);
}
style>
在这个示例中,当 show 的值为 true 时,元素会执行旋转动画并插入到 DOM 中。当 show 的值为 false 时,元素会执行旋转动画并从 DOM 中移除。在任何情况下,transition 组件都会确保在元素插入或移除之前应用过渡动画。
总结:为true,插入动画;为false,移除动画。不论是插入还是移除,两个过程都是一个完整的循环,也就是说,在上面的例子中,插入过程是旋转一周,而移除过程同样是旋转一周,效果是一样的,除非我们将两个过程分别单独设置。如果是如下一样并列设置
那么,两个过程动画效果将一模一样。此外,插入的开始状态至插入的结束状态为一个完整旋转一周的循环,同理,移除的开始至结束两个状态也是一样,并且,插入的结束状态和移除的开始状态是同一个值,插入的开始状态和移除的结束状态也是同一个值。