Vue介绍
vue是一款用于构建用户界面的JavaScript框架。它基于标准HTML、CSS和JavaScript构建,提供了一套声明式的、组件化的编程模型,帮助更高效的开发用户界面。
vue的准备工作
前提条件:
- 熟悉命令行
- node.js版本要大于15.0
查看node.js版本可以用 node -v
创建vue项目:
确保你安装了最新版本的 Node.js,并且你的当前工作目录正是打算创建项目的目录。在命令行中运行以下命令
npm init vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:
安装完之后,使用cd进入目录,npm install安装,npm run dev执行:
cd <your-project-name>
npm install
npm run dev
vue项目的目录结构
vue的模版语法
Vue使用一种基于HTML的模版语法,让我们能够声明式地将组件实例的数据绑定到呈现的DOM上。所有的Vue模版都是语法层面合法的HTML,可以被符合规范的浏览器和HTML解析器解析。
注意:爽大括号中间的数据只支持单一表达式并且有返回值,不可换行,不可以写成 var a=1 这种形式
<template>
<div>
{{ msg }}
</div>
</template>
<script>
export default {
data(){
return{
msg:"神奇的语法"
}
}
}
</script>
属性绑定 v-bind:
双大括号不能在HTML 属性中使用,要想响应式的绑定一个html标签的属性,要使用v-bind指令,v-bind指令是单向绑定,由data数据单向控制html属性。其中v-bind可以简写成:
v-bind绑定的元素如果值为null或者undefined,那么该属性将会从渲染的元素上移除。
<template>
<!-- 正常写法 -->
<div v-bind:id="dynamicId" v-bind:class="dynamicClass">测试1</div>
<!-- 简写 -->
<div :id="dynamicId" :class="dynamicClass">测试2</div>
</template>
<script>
export default {
data(){
return{
dynamicClass:"active",
dynamicId:"appid",
}
}
}
</script>
条件渲染 v-if:
在vue中提供了条件渲染,类似于javascript中的条件语句:
- v-if
- v-else
- v-else-if
- v-show
v-if:
<template>
<h3>条件渲染</h3>
<br/>
<div v-if="flag" >我是v-if</div>
<div v-else >我是else</div>
</template>
<script>
export default {
data(){
return{
flag:true
}
}
}
</script>
v-else-if:
<template>
<h3>条件渲染</h3>
<br/>
<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-if="type==='D'" >D</div>
<div v-else>E</div>
</template>
<script>
export default {
data(){
return{
type:'a'
}
}
}
</script>
v-show:
<template>
<h3>条件渲染</h3>
<br/>
<div v-show="type">我是v-show</div>
</template>
<script>
export default {
data(){
return{
type:true
}
}
}
</script>
v-if和v-show的区别:
v-if是真实的条件渲染,它确保了在切换时,条件区内的事件监听器和子组件都会被销毁与重建。
v-if是惰性的:如果在初次渲染时条件值为false,则不会做任何事。条件区块内只有当条件首次为true时才会被渲染。
相比之下,v-show要简单的多,v-show无论什么时候都会被渲染,只是css里的display属性被修改。
总的来说,v-if有更多的切换开销,而v-show有更多的初始渲染开销,如果需要频繁切换,用v-show,如果条件很少被改变,就用v-if。
事件处理:
内联事件处理,把事件的处理方法直接写在标签属性里,写成表达式,用于简单计算
<template>
<h3>内联事件处理</h3><br/>
<button @click="count++">Add 1</button>
<p>Count is:{{ count }}</p>
</template>
<script>
export default{
data(){
return{
count:0
}
}
}
</script>
方法事件处理,把方法写到methods函数里面,独立成一个方法去处理事件,实际开发用这种方式比较多
<template>
<h3>方法事件处理</h3><br/>
<button @click="addCount">Add 1</button>
<p>Count is:{{ count }}</p>
</template>
<script>
export default{
data(){
return{
count:0
}
},
methods:{
addCount(){
this.count++;
}
}
}
</script>
事件传参:
<template>
<h3>方法事件处理-传递参数</h3><br/>
<button @click="addCount('hello')">Add 1</button>
</template>
<script>
export default{
data(){
return{
count:0
}
},
methods:{
addCount(msg){
console.log(msg)
}
}
}
</script>
事件传参,获取列表数据:
<template>
<h3>事件传参处理-点击获取列表数据</h3><br/>
<p @click="getNameHandler(item,$event)" v-for="(item,index) of names" :key="index">{{ item }}</p>
</template>
<script>
export default{
data(){
return{
names:["iwen","ime","frank"]
}
},
methods:{
getNameHandler(name,e){
console.log(name)
console.log(e)
}
}
}
</script>
事件修饰符:
在处理事件时调用event.preeventDefault()或者event.stopPropagation()是很常见的。尽管我们可以直接在方法内调用,但如果方法能更关注数据逻辑而不用去处理DOM事件的细节会更好
为了解决这一问题,Vue为v-on提供了事件修饰符,常用有以下几个:
- stop
- prevent
- one
- enter
阻止默认事件:
<template>
<h3>事件修饰符</h3><br/>
<!-- 通过@click.prevent来阻止页面跳转 -->
<a @click.prevent="clickHandle" href="http://www.baidu.com">点我进百度</a>
</template>
<script>
export default{
data(){
return{
}
},
methods:{
clickHandle( e){
// e.preventDefault();
console.log("点击了超链接")
}
}
}
</script>
常用的事件修饰符:
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
数组侦测变化(动态修改数组的数据)
变更方法:
Vue能够侦听响应式数组的变更方法,并在他们被调用时触发相关的更新。这些变更方法包括:
- push():将一个或者多个元素添加到数组的末尾,并返回数组的新长度。
- pop():移除数组中末尾的元素。
- shift():移除数组的第一个元素。
- unshift():将一个或者多个元素添加到数组的开头,并将其他元素向后移。
- splice():从指定的索引位置开始,删除指定数量的元素,并可选的插入新的元素。可以实现删除、插入和替换数组中的元素。
array.splice(start, deleteCount, item1, item2, …);
其中,第一个参数start代表要操作的数组的索引位置,第二个参数代表删除元素的数量,当deleteCount=0时,代表添加元素,第三个以及之后的参数就是要操作的元素内容
- sort():对数组进行排序,如果是字符串的话,就根据unicode进行编码,然后排序,也可以根据自定义的排序函数进行排序
- reverse():反转数组的元素顺序,将数组元素按相反的顺序排列。
<template>
<h3>数组变化侦测</h3><br/>
<button @click="addList" >执行数据操作函数</button>
<ul>
<li v-for="(item,index) of names " :key="index">{{ item }}</li>
</ul>
</template>
<script>
export default{
data(){
return{
names:["zhangsan","lisi","frank","naruto","sasuke","sakura"]
}
},
methods:{
addList(){
// this.names.reverse()
// this.names.sort();
// this.names.splice(0,1,'pear')
// this.names.splice(2,1)
// this.names.splice(1,0,"naruto","sasuke")
// this.names.unshift("naruto","sasuke")
// console.log(this.names.shift())
// this.names.pop();
// this.names.push("sakura")
}
}
}
</script>
替换一个数组
有一些不可变方法,这些都不会更改原数组,而是返回一个新数组。当遇到是非变更方法时,我们会把旧数组替换成新的:
- concat() : 在数组的尾部添加元素
- filter() : 通过方法过滤元素的内容
- slice() : 截取数组中特定的片段
<template>
<h3>数组变化侦测</h3><br/>
<button @click="addList" >执行数据操作函数</button>
<ul>
<li v-for="(item,index) of names " :key="index">{{ item }}</li>
</ul>
</template>
<script>
export default{
data(){
return{
names:["zhangsan","lisi","frank","naruto","sasuke","sakura"]
}
},
methods:{
addList(){
// this.names=this.names.slice(3,5)
// this.names = this.names.filter((item) => item.length > 5);
// this.names= this.names.concat(["kakachi"])
}
}
}
</script>
计算属性:
模版中的表达式虽然方便,但也只能来做简单的操作。如果在模版中写太多逻辑,会让模版变得臃肿,难以维护。因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑:
<template>
<h3>计算属性</h3>
<p>{{ itbaizhanContent }}</p>
</template>
<script>
export default{
data(){
return{
itbaizhan:{
name:"百战程序员",
content:["前端","Java","python"]
}
}
},
computed:{
itbaizhanContent(){
return this.itbaizhan.content.length>0?"yes":"no"
}
}
}
</script>
计算属性和方法的区别(两者都可以实现相同的功能):
计算属性:计算属性有自动缓存的功能,他会根据依赖的响应式数据进行缓存,只有当依赖的数据发生变化时,才会重新计算,一般情况下,对数组进行过滤,映射,根据响应式数据的变化计算其他属性时使用
方法:方法不具备缓存功能,每次调用方法时都会执行其中的代码,但是复杂数据还是要交给方法来做,适合每次调用都需要执行代码的情况,或者需要手动传递参数的情况,比如点击某个按钮触发什么时间,需要根据用户输入计算结果等。
Class绑定:
数据绑定的一个常见需求场景是操纵元素的CSS class列表,因为class是属性,我们可以和其他属性一样使用v-bind将他们和动态的字符串绑定。但是在处理比较复杂的绑定时,通过拼接生成字符串是比较麻烦且容易出错的。因此,vue专门为class的v-bind用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或者数组。
单个对象的绑定:
<template>
<!-- class绑定用来绑定class属性是否生效 -->
<!-- 对象写法,值为true,键显示,值为false,键不显示 -->
<div :class="{'active':isActive,'text-danger':hasError}">isActive</div>
</template>
<script>
export default{
data(){
return{
isActive:false,
hasError:true
}
}
}
</script>
<style>
.active{
background-color: aqua;
color: burlywood;
}
</style>
多个对象写法:
<template>
<div :class="classObject">isActive</div>
</template>
<script>
export default{
data(){
return{
classObject:{
active:true,
'text-danger':true
}
}
}
}
</script>
绑定数组:
<template>
<div :class="[activeClass,errorClass]">isActive</div>
</template>
<script>
export default{
data(){
return{
activeClass:true,
errorClass:true
}
}
}
</script>
Style样式 绑定:
数据绑定的一个常见需求场景是操纵元素的CSS style列表,因为style是属性,我们可以和其他属性一样使用v-bind将他们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接字符串的方式可能出错且麻烦,因此,Vue专门为style的v-bind用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或者数组。
绑定对象:
<template>
<div :style="{color:activeColor,fontSize:fontSize+'px'}">Style样式绑定</div>
</template>
<script>
export default{
data(){
return{
activeColor:'red',
fontSize:30
}
}
}
</script>
<template>
<div :class="[activeClass,errorClass]">isActive</div>
</template>
<script>
export default{
data(){
return{
activeClass:true,
errorClass:true
}
}
}
</script>
侦听器:
用来监听页面数据的变化,页面的数据一旦发生变化,监听器就可以监听到,并做出相应的反应,监听器并不是监听所有数据,而是去监听那些具有响应式的数据。
注意:watch对象的函数监听数据的方法必须与data里的数据对象名称一致。
侦听器一般都和方法配合使用,当监听到数据发生变化时,调用方法执行相应操作,不建议在监听器里直接修改数据
<template>
<h3>侦听器</h3><br/>
<p>{{ msg }}</p>
<button @click="change" >修改数据</button>
</template>
<script>
export default{
data(){
return{
msg:"hello"
}
},
methods:{
change(){
this.msg="world"
},
alert(){
alert("数据改变了")
}
},
watch:{
msg(newValue,oldValue){
console.log(newValue)
console.log(oldValue)
this.alert();
}
}
}
</script>
表单输入绑定(双向绑定)(v-model):
在前端处理表单时,我们经常需要将表单输入框的值赋值给js中相应的变量,手动连接值绑定和更改事件监听器可能会很麻烦,v-mode可以很简单的做到这一点:
<template>
<input type="text" v-model="msg"/>
<p>{{ msg }}</p>
</template>
<script>
export default{
data(){
return{
msg:""
}
}
}
</script>
v-model的修饰符:
v-model提供了以下几种修饰符,就不再写demo练习,只解释一下作用:
- .lazy(懒加载) 数据不再同时获取,而是在失去焦点,点击回车等时在获取
- .number(获取数字) 只从输入框获取数字,字母特殊符号等不要
- .trim(去掉前后空格)去掉输入框中的前后空格
模版引用(ref直接操作dom元素)
ref虽然能直接操作dom 元素,可用性很高,但是实际开发时还是尽量少用ref,因为直接操作dom很消耗资源,尽量使用vue提供的操作虚拟dom的方法
<template>
<div ref="container" class="container">{{ content }}</div>
<button @click="getElementHandle">获取元素</button>
</template>
<script>
export default{
data(){
return{
content:"内容"
}
},
methods:{
getElementHandle(){
console.log(this.$refs.container)
this.$refs.container.innerHTML="哈哈哈"
}
}
}
</script>
Vue组件基本知识:
组件最大的优势就是可复用性
当使用构建步骤时,我们一般会将Vue组件定义在一个单独的 .vue 文件中,这被叫做单文件组件,简称(SFC)
<!-- template模版承载所有html标签 -->
<template>
<div>承载标签</div>
</template>
<!-- 承载所有的业务逻辑 -->
<script>
export default{}
</script>
<!-- 承载所有的样式 -->
<style>
</style>
vue引入子组件的过程:
- 通过import语句引入组件
- 通过components注入组件
- 通过标签的形式使用组件
<template>
<div class="container" >{{ msg }}</div>
<!--第三步,在模版中使用组件 -->
<compo2/>
</template>
<script>
//第一步,import引入组件
import compo2 from './compo2.vue';
export default{
// 第二部,在components对象中注入组件
components:{
compo2
},
data(){
return{
msg:"组件基本组成"
}
}
}
</script>
<style>
.container{
font-size: 30px;
color:red;
}
</style>
注意,在vue3中,有更简单的办法,省略第二步:
如下图,在script标签里设置setup,就可以不用注入组件,引入组件直接使用即可
以及在style中有一个scoped属性:
组件嵌套关系:
组件允许我们将UI划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构。
这和我们嵌套HTML元素的方式类似,Vue实习拿了自己的组件模型,使我们可以在每个组件内封装自定义内容和逻辑
下面将编写一个基本的页面:
Header:
<template>
<h3>Header</h3>
</template>
<style scoped >
h3{
width:100%;
height:100px;
border:5px solid #999;
text-align:center;
line-height: 100px;
box-sizing: border-box;
}
</style>
Main:
<template>
<div class="main">
<h3>Main</h3>
<ArticleVue/>
<ArticleVue/>
</div>
</template>
<script setup >
import ArticleVue from "./Article.vue";
</script>
<style scoped>
.main{
float:left;
width:70%;
height: 400px;
border:5px solid #999;
box-sizing: border-box;
}
</style>
Aside:
<template>
<div class="aside">
<h3>Aside</h3>
<ArticleVue/>
<ArticleVue/>
</div>
</template>
<script setup >
import ArticleVue from "./Article.vue";
</script>
<style scoped>
.aside{
float: right;
width: 30%;
height: 400px;
border:5px solid #999;
box-sizing: border-box;
}
</style>
Article:
<template>
<h3>Item</h3>
</template>
<style scoped>
h3{
width:80%;
margin:0 auto;
text-align: center;
line-height: 100px;
box-sizing: border-box;
margin-top: 10px;
background: #999;
}
</style>
<script>
</script>
组件注册方式:
一个Vue组件在使用前需要先被注册,这样Vue才能在渲染模版时找到其对应的实现。组件的注册有两种方式:全局注册和局部注册:
全局注册:
先import对应的组件,然后用app.component挂载组件:
// import './assets/main.css'
import MainVue from "./components/compo/main.vue";
import Header from "./components/compo/header.vue";
import Aside from "./components/compo/Aside.vue";
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.component("MainVue",MainVue)
.component("Header",Header)
.component("Aside",Aside)
app.use(createPinia())
app.use(router)
app.mount('#app')
局部注册:
全局注册虽然很方便,但是有以下几个问题:
- 全局注册,但并没有被使用的组件无法再生产打包时被自动移除。如果全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包之后的JS文件中
- 全局注册在大型项目中使项目的依赖关系变得不那么明确,在父组件使用子组件的时候,不太容易定位子组件的位置与实现。和使用过多的全局变量一样,可能会影响长期的可维护性
组件间传递数据:
组件与组件之间并不是完全独立的,而是有交集的,那就是组件与组件之间是可以传递数据的,传递数据的解决方案就是 props
普通传递参数方式:
<template>
<div>Parent</div>
<Child title="Parent数据" demo="测试" />
</template>
<script >
import Child from './Child.vue';
export default{
data(){
return{}
},
components:{
Child
}
}
</script>
<template>
<div>Child</div>
<p>{{ title }}</p>
<p>{{ demo }}</p>
</template>
<script>
export default{
data(){
return{}
},
props:["title","demo"]
}
</script>
动态传递数据:
<template>
<div>Parent</div>
<Child :title="msg" />
</template>
<script >
import Child from './Child.vue';
export default{
data(){
return{
msg:'动态传递数据'
}
},
components:{
Child
}
}
</script>
注意事项:props传值只能从父级传递到子级
props传值,不仅可以传递字符串类型的数据,还可以传递其他类型数据,例如:数字,对象,数组,但是实际上任何类型的值都可以作为props的值被传递
组件传值props校验:
Vue组件可以更细致地声明对传入的props的校验要求
通过设置子组件的props属性来进行传值校验和设置默认值
组件事件(子组件向父组件传值):
在组件的模版表达式中,可以直接使用$emit方法触发自定义事件
触发自定义事件的目的是组件之间传递数据
使用场景:子组件给父组件传值
父组件:
<template>
<h3>组件事件(子传父)</h3>
<!-- 通过属性绑定把msg的值传递到子组件的title属性上,用v-bind -->
<CompoB @someEvent="getHandle" />
</template>
<script>
import CompoB from '@/components/validate/CompoB.vue'
export default{
data(){
return{
msg:"父组件传的值"
}
},
components:{
CompoB
},
methods:{
getHandle(data){
console.log("触发了")
console.log(data)
}
}
}
</script>
子组件:
<template>
<h3>子组件</h3>
<button @click="clickEventHandle">给父组件传值</button>
</template>
<script>
export default{
data(){
return{}
},
methods:{
clickEventHandle(){
this.$emit("someEvent","传递的数据")
}
}
}
</script>
组件之间传递数据的方案:
父传子:props
子传父:自定义事件(this.$emit)
组件事件配合v-model使用实现父子组件值同步:
父组件:
<template>
<div>Main</div>
搜索: <search @searchEvent="getsearch" />
<p>{{ search }}</p>
</template>
<script>
import search from './search.vue';
export default{
data(){
return{
search:''
}
},
components:{
search
},
methods:{
getsearch(data){
this.search=data;
}
}
}
</script>
子组件:
<template>
<input type="text" v-model="search"/>
</template>
<script>
export default{
data(){
return{
search:""
}
},
watch:{
search(newValue,oldValue){
this.$emit("searchEvent",newValue)
}
}
}
</script>
组件传递数据(props实现子传父):
利用函数带参数的方式来实现:
父组件:
<template>
<div>CompoC</div>
<p>{{ msg }}</p>
<CompoD title="标题" :onEvent="dataFn" />
</template>
<script >
import CompoD from './CompoD.vue';
export default{
data(){
return{
msg:'',
}
},
components:{
CompoD
},
methods:{
dataFn(data){
console.log(data)
this.msg=data
}
}
}
</script>
子组件:
<template>
<div>CompoD</div>
<p>{{ title }}</p>
<p>{{ onEvent('我是父传子props的参数亦甜') }}</p>
</template>
<script >
export default{
data(){
},
props:{
title:String,
onEvent:Function
}
}
</script>
插槽Slots:
我们已经了解到组件能够接受任意类型的js值作为props,但组件要如何接受模版内容呢?在某些场景中,我们可能想要为子组件传递一些模版片段,让子组件在他们的组件中渲染这些片段:
解释:父组件向子组件中传递html标签:
父组件:
<script setup>
import SlotsBaseAVue from './components/slot/SlotsBaseA.vue'
</script>
<template>
<SlotsBaseAVue>
<div>
<h3>插槽标题</h3>
<p>插槽内容</p>
</div>
</SlotsBaseAVue>
</template>
子组件:
<template>
<slot></slot>
</template>
<script>
export default {
}
</script>
< slot >元素是一个插槽出口,标识了父元素提供的插槽内容将在哪里被渲染。
插槽的渲染作用域:
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模版中定义的
父组件:
<template>
<div>我是父组件</div>
<SlotB>
<div>{{ msg }}</div>
</SlotB>
</template>
<script>
import SlotB from './SlotB.vue';
export default {
data(){
return{
msg:'我是插槽'
}
},
components:{
SlotB,
}
}
</script>
子组件:
<template>
<div>我是子组件</div>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
}
</script>
具名插槽:
父组件:
<template>
<div>
<NameB>
<!-- 使用具名插槽1,传递头部内容 -->
<template v-slot:header1>
<h1>这是第一个具名插槽传递给头部的内容</h1>
</template>
<!-- 使用具名插槽2,传递底部内容 -->
<template v-slot:footer1>
<p>这是第二个具名插槽传递给底部的内容</p>
</template>
</NameB>
</div>
</template>
<script>
import NameB from '@/views/NameSlot/NameB.vue'
export default{
components:{
NameB
}
}
</script>
子组件:
<template>
<div>
<!-- 这是具名插槽 -->
<slot name="header1"></slot>
<div>
<!-- 子组件的内容 -->
<p>这是子组件的内容</p>
</div>
<!-- 这是具名插槽2 -->
<slot name="footer1"></slot>
</div>
</template>
<script>
</script>
插槽中的数据传递(父子都需要向插槽内传参):
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法让子组件渲染时将一部分数据提供给插槽
可以像对组件传递props那样,向一个插槽的出口上传递attributes。
解释:插槽的内容同时需要父子组件的数据
如何实现:子组件通过attributes先把数据传递给父组件,父组件再去把子组件的数据传递到插槽
父组件:
<template>
<div>
<h3>slot再续集</h3>
<h3>父组件</h3>
<SlotFVue v-slot="slotProps">
<h3>{{ currentTest }}-{{ slotProps.msg }}</h3>
</SlotFVue>
</div>
</template>
<script>
import SlotFVue from "./SlotF.vue";
export default {
components: {
SlotFVue,
},
data() {
return {
msg: '插槽内容续集',
currentTest: '测试内容',
};
},
};
</script>
子组件:
<template>
<div>
子组件
<slot :msg="child"></slot>
</div>
</template>
<script>
export default {
data() {
return {
child: '子组件元数据',
};
},
};
</script>
组件生命周期:
每个Vue组件实例在创建时都需要经理一系列的初始化步骤,比如设置好数据侦听,编译模版,挂载实例到DOM,以及在数据改变时更新DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会再特定阶段运行自己的代码。
各个生命周期的意义:
- setup:setup函数是Vue3中新增的生命周期钩子函数,用于取代Vue2中的beforeCreate和created函数。在setup函数中,可以进行组件的初始化工作,包括相应式数据的定义、引入其他逻辑等。但是setup函数不能异步,并且他没有访问this的上下文。
- onBeforeMount:在组件挂载之前调用。
- onMountd:在组件被挂载之后调用。
- onBeforeUpdate:在组件更新之前调用,即在重新渲染之前调用。
- onUpdated:在组件更新之后调用,即在重新渲染之后调用。
- onBeforeMount:在组件卸载之前调用。
- onUmmounted:在组件卸载之后调用。
- onErrorCaptured:捕获组件及其子组件的所有错误。
生命周期基础:
以下代码演示了生命周期函数是如何使用的:
<template>
<h3>组件的生命周期</h3>
<!-- 放一个按钮去更新msg的消息,触发生命周期里的更新函数 -->
<button @click="onclick" >点我改变</button>
<p>{{ msg }}</p>
</template>
<script>
import { setMapStoreSuffix } from 'pinia';
/**
* 生命周期函数:
* 创建期:beforeCreate created,在vue3中用setup代替
* 挂载期:beforeMount mounted
* 更新器:beforeupdate updated,指组件的重新渲染
* 销毁期:beforeUnmount unmounted
*/
export default{
data(){
return{
msg:'我是消息'
}
},
methods:{
onclick(){
this.msg='我是改变后的消息'
}
},
beforeCreate(){
console.log('组件创建之前');
},
created(){
console.log('组件创建之后');
},
beforeMount(){
console.log('组件渲染之前');
},
mounted(){
console.log('组件渲染之后');
},
beforeUpdate(){
console.log('组件更新之前');
} ,
Updated(){
console.log('组件更新之后');
},
beforeUnmount(){
console.log('组件销毁之前');
},
unmounted(){
console.log('组件销毁之后');
}
}
</script>
生命周期函数的具体应用:
组件的生命周期会随着我们对Vue的了解越来越多,也会越来越重要,我们先讲两个常见的应用:
- 通过ref获取元素DOM结构
- 模拟网络请求渲染数据
通过ref获取元素DOM元素:
<template>
<h3>组件生命周期的应用</h3>
<p ref="name">百战程序员</p>
</template>
<script>
export default{
beforeMount(){
console.log(this.$refs.name);
},
mounted(){
console.log(this.$refs.name);
}
}
</script>
模拟网络请求渲染数据:
<template>
<h3>组件生命周期函数应用</h3>
<p ref="name" >百战程序员</p>
<ul>
<li v-for="(item,index) of banner" :key="index">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
</li>
</ul>
</template>
<script>
export default{
data(){
return{
banner:[]
}
},
beforeCreate(){
},
created(){
//在组件创建完成之后发送网络请求获取数据
this.banner=[
{
"title": "中国",
"content": "中国位于东亚,是世界上最古老的文明之一,拥有悠久的历史和丰富的文化遗产。中国是世界上面积第三大、人口最多的国家,也是世界第二大经济体。"
},
{
"title": "美国",
"content": "美利坚合众国是北美洲的一个国家,是世界上最强大的国家之一,具有丰富的自然资源和多元的文化。美国是联邦共和制国家,以自由、民主和创新著称。"
},
{
"title": "日本",
"content": "日本位于东亚,是一个拥有悠久历史和独特文化的岛国。日本是世界上最先进的科技国家之一,同时也以其丰富的传统文化和美丽的自然景观而闻名。"
}
]
},
beforeMount(){
console.log(this.$refs.name);
},
mounted(){
console.log(this.$refs.name);
}
}
</script>
动态组件(通过按钮去改变组件):
父组件:
<template>
<button @click="change" >按钮切换组件</button>
<component :is="tabComponent"></component>
</template>
<script>
import MoveComA from '@/views/MoveCom/MoveComA.vue'
import MoveComB from '@/views/MoveCom/MoveComB.vue'
export default{
data(){
return{
tabComponent:MoveComB
}
},
methods:{
change(){
this.tabComponent=this.tabComponent == "MoveComA" ? "MoveComB" : "MoveComA"
}
},
components:{
MoveComA,
MoveComB
}
}
</script>
子组件1:
<template>
<h3>ComponentA</h3>
</template>
子组件2:
<template>
<h3>ComponentB</h3>
</template>
选项式API和组合式API的区别:
选项式API:
什么是选项式API:在vue2中的项目使用的是选项API的写法
代码风格:data选项写数据,methods选项写函数、、、、一个功能逻辑的代码分散
优点:易于学习和使用,写代码的位置已经约定好了
缺点:代码组织性差,相似的逻辑代码不利于复用,逻辑复杂代码多了不好阅读
虽然提供了mixns用来封装逻辑,但是出现数据函数覆盖的概率很大,不好维护
export default {
data() {
return {
count: 1
}
},
// `mounted` 是生命周期钩子,之后我们会讲到
mounted() {
// `this` 指向当前组件实例
console.log(this.count) // => 1
// 数据属性也可以被更改
this.count = 2
}
}
组合API:
什么是组合式API:在vue3中使用的就是组合API的写法
代码风格:一个功能逻辑的代码组织在一起(包括数据,函数等)
优点:功能逻辑复杂繁多的情况下,各个功能逻辑代码组织在一起,便于方便阅读维护
缺点:需要有良好的代码分析能力和组织能力(在vue3中也可以用选项式API的写法,但不推荐)
<template>
<div>我是组合式基础</div>
<div>{{ count }}</div>
</template>
<script>
import {ref,defineComponent} from 'vue'
export default defineComponent({
setup() {
const count=ref(3)
return{
count
}
},
})
</script>
组合式API详解:
ref ( )
在组合式API中,推荐使用ref ( ) 函数来声明响应式状态:
import {ref} from 'vue'
const count=ref(3)
要访问组件模版中的ref,请从setup ( )函数中声明并返回他们:
当在模版中使用ref时,不需要通过 .value去访问,为了方便起见,当在模版中使用时,ref会自动解包,会自动转换。
<template>
<!-- 在模版中引入相应的数据 -->
<div>{{ count }}</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
//setup是一个特殊的钩子,专门用于组合式API
setup() {
//通过const 响应式数据声明count数据
const count=ref(0)
//通过return将ref的数据暴露给模版
return{
count
}
},
})
</script>
关于setup在vue3中的语法糖:
未使用语法糖的格式:
<template>
<div>{{ msg }}</div>
</template>
<script>
import { ref,defineComponent } from 'vue'
export default defineComponent({
setup() {
const msg=ref('hello,world')
return{
msg
}
},
})
</script>
使用语法糖的形式:
相对来说就简单多了,推荐使用语法糖
<template>
<div>{{ msg }}</div>
</template>
<script setup>
import { ref} from 'vue'
const msg=ref('hello,world')
</script>
setup语法糖中写入函数的格式:
<template>
<div>响应式基础-事件响应-语法糖格式</div>
<div>count的值:{{ count }}</div>
<button @click="increment" >点击我更改count的值</button>
</template>
<script setup>
import { ref } from 'vue'
const count=ref(0)
/**
* 以下是事件的两种写法,用那种都行
* 上面是箭头函数的写法,下面是普通函数的写法
*/
const increment = () =>{
count.value++
}
function incrementd(){
count.value++
}
</script>
为什么要使用ref?
为什么需要使用带有 .value的ref,而不是普通的变量?为了解释这一点,需要了解一下Vue的响应式系统是如何操作的。
当你在模版中使用了一个ref,然后改变了这个ref的值时,Vue会自动检测到这个变化,并且相应地更新DOM,这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue会追踪在渲染过程中使用的每一个ref。然后,当一个ref被修改时,他会触发追踪他的组件的重新渲染过程。
在标准的JavaScript中,检测普通变量的访问或者修改是行不通的,但是可以拦截属性的get和set操作。
.value 属性给予了Vue一个机会来检测ref何时被访问或修改,在其内部,Vue在他的gtter方法中执行追踪,在它的setter中执行触发,从概念上讲,可以将ref看做一个这样的对象:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
reactive ( )
还有另一种声明响应式状态的方式,即使用reactive()API。与将内部值包装在特殊对象中的ref不同,reactive()将会使对象本身具有响应性:
import { reactive } from 'vue'
// 推导得到的类型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })
ref和reactive的区别:
- 数据类型不同:ref用于包装基本数据类型,比如数值,字符串,布尔值等,而reactive用于包装对象和数组等复杂的数据。
- ref需要在模版中使用ref指令以及在js代码中使用ref函数进行创建和使用,而reactive需要用vuejs提供的reactive函数进行包装和创建
- 访问方式不同,对于ref创建的数据,需要通过.value属性去访问和修改值,而对于reactive创建的响应式对象,直接访问其属性或者调用其方法。
- 设计理念:ref主要为了解决单一数据的响应式问题,而reactive则是为了解决复杂数据结构的响应式问题
关于vue3中nextTick()的问题
本文将介绍nextTick的工作原理:
nextTick的使用时机:
首先记住:nextTick所制定的回调会再浏览器更新DOM完毕之后再执行。
举个例子:
在我自己做的一个单页面应用中,我有一个需求,当从后端请求回数据后,马上就对页面进行更新。比如,我要点击操作里的删除操作,前端删除数据后就向后端发送请求,后端删除数据后返回数据,如果向马上显示到页面上,应该如何去做?
可能你会想使用v-for遍历data这个数据将他显示出来,直接把后端返回的数据直接赋值给data,但是这样做不可以,因为这样做导致数据变了,但是页面显示的数据并没有发生变化,因为vue更新DOM是异步的,无法通过同步代码赋值后马上去更新页面。所以即使删除成功了页面也不会显示出来,即使显示删除成功,页面也不会更新。
如何更新
我们的需求是,请求回数据后马上更新页面,首先理解几个点。
Vue更新DOM是异步更新
如图官网的描述,解释一下这段话,直接看可能有些懵,前两句话,关于同步和异步,以及提到的"事件循环",这就要从JS的运行机制说起,戳我前往。第三句话,“如果同一个 watcher 被多次触发,只会被推入到队列中一次”,这是什么意思呢,也在我的文章中可找到,戳我前往。
下面这句话是官网的原文:
异步更新队列
可能你还没有注意到,Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件中发生的所有数据变更。如果一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时取出重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行时机工作。Vue在内部队列尝试使用原生的 Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn,0)来代替。
理解执行时机
nextTick内部就是调用宏任务和微任务来完成事件调用的机制,让nextTick里的回调在一个事件循环的最后执行。为什么要放在最后呢?在最后意味着在所有异步任务之后,记得上一点吗,vue更新DOM是异步更新操作,而我们把nextTic里的回调放在了所有异步任务的最后,这样就解释了开始的那句话,nextTic所制定的回调会在浏览器更新DOM完毕之后再执行。
回到例子实现需求
我们想要实现需求,本例使用热更新,即只更新局部组件。
我直接把我的子组件全放在router-view里的,通过变化v-if来实现组件更新。
看到这里可能会有人问,这不就是先把v-if设为false,在设置为true来实现吗,要不要nextTick都一样,实际上并不是如此。
还记得刚才说过,如果同一个watcher被多次触发,只会被推入到队列中一次,vue会把所有代码都执行了再去渲染页面,所以这两条false和true的代码相当于只执行了最后一条true,false都没有执行,被抛弃了,所以页面相当于什么都没做,原始值是true,现在还是true。
加了个nextTick就不一样了,这段代码就相当于告诉vue,先执行false的代码,先不要管我nextTick,去渲染完页面了之后再来执行我的nextTick里面的代码。
这样来说可能就比较明白了,true代码是页面渲染过后再执行的,所以我们肉眼看其实已经删除货物,页面马上就更新了,其实其中经过了先把router-view里的子组件取消掉,即v-if=false,然后再让v-if=true,那么v-if就是创建组件,相当于router-view里的内容被重新创建了,所以此时的数据也是最新的。
官网中对于nextTick()的解释:
等待下一次DOM更新刷新的工具方法。
- 类型:
function nextTick(callback?: () => void): Promise<void>
- 详细信息:
当你在Vue中更改响应式状态时,最终的DOM更新并不是同步生效的,而是由Vue将他们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick() 可以在状态改变后立即使用,以等待DOM更新完成。你可以传递一个回调函数作为参数,或者await返回的Promise。 - 示例:
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>