Vue3
回顾之前的学习
- node.js:我们的服务器是MVC的模式,服务器直接将渲染后的页面发送给客户端
- ajax:我们的服务器是rest风格的服务器,服务器将数据以json形式发送给客户端,由客户端去渲染。
- 此时我们的模式改变成mvvm,我们需要view model来将服务器返回的模型变成视图,在这个学习阶段我们通过DOM来实现
- 使用DOM实现这个功能有两个缺点:麻烦和性能不好,所以我们需要前端js框架来代替DOM
什么是vue
vue是一个前端的框架,主要负责帮助我们构建用户界面(user-interface)
MVVM:Model - View - View Model
vue负责vm的工作(视图模型),通过vue可以将视图和模型相关联。
- 当模型发生变化时,视图会自动更新
- 也可以通过视图去操作模型
vue思想:
- 组件化开发:把整个网页拆分成多个组件
- 声明式的编程
- 命令式编程(Imperative):详细的命令机器怎么(How)去处理一件事情以达到你想要的结果(What)
- 声明式编程( Declarative):只告诉你想要的结果(What),机器自己摸索过程(How)
- 渐进式:基于其他前端框架的项目也可以部分采用vue编写
引入vue3
直接在网页中使用(像jQuery一样)
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
//创建一个按钮,可以显示按钮的点击的次数
//创建组件
const Root = {
data() {
return {
count: 0 // 记录点击次数
}
},
template:
`<button @click='count++'>点我一下</button> - 点了{{count}}次`
}
//创建app实例并挂载
Vue.createApp(Root).mount("#app")使用vite
npm install vite -D
npm install vue
// 这里引入的vue,默认不支持通过template属性来设置模板
import {createApp} from "vue/dist/vue.esm-bundler.js"
const App={
data(){
return{
message:"Vue3"
}
},
template:`<h1>hello{{message}}</h1>`
}
createApp(App).mount("#app")代码:
// 组件,就是一个普通js对象
const App = {}
// 返回一个应用实例,将根组件关联到应用上
const app = createApp(App)
// 将应用挂载到页面,生成一个组件实例(视图模型),组件实例类型是一个Proxy对象(代理对象)
const vm=app.mount("#root")
//该方法接收一个参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串自动创建vue项目
npm init vue@latest
npm install
安装依赖
选项式API
组件
vue3中组件就是一个普通的js对象
组件用来创建组件实例(vm),组件是组件实例的模板
组件 --> 组件生成组件实例 --> 虚拟DOM --> DOM(在页面中呈现)
组件的作用:组件使得我们可以生成多个组件实例在不同的地方使用,组件实例相互独立不会互相影响,大大提高了代码的复用性
分类:
根组件:
createApp(根组件名)
直接使用,一个项目一般只有一个根组件(项目部分使用vue可以创建多个app)子组件:
- 引入子组件
- 在组件中注册子组件
import MyButton from "./components/MyButton"
export default {
data() {
return {
message: "Vue3"
}
},
//注册组件
components:{
MB:MyButton
},
template: `<h1>hello{{message}}</h1>
<MB></MB>
<MB></MB>
<MB></MB>
`
}
data
data是一个方法,需要一个对象作为返回值,用来指定实例对象中的响应式属性
data方法返回的对象,vue会对该对象进行代理,从而将其转换为响应式数据,响应式数据可以直接通过组件实例访问
- 响应式数据改变,页面会跟着改变
- 直接向组件实例中添加的属性不会被vue所代理,不是响应式数据,修改后页面不会改变
- 组件实例
vm.$data
才是真正的代理对象,我们可以使用这个属性动态添加响应式数据,但不建议这么做
在data中,this就是当前的组件实例vm
- vue给组件中的data方法绑定了this为vm
如果使用箭头函数,则无法通过this来访问组件实例
- 绑定对箭头函数无效
内置第一个参数就是组件实例vm,箭头函数可以以此来访问vm
在vue中使用选项式API,尽量减少箭头函数的使用
注意点
- 返回唯一对象
<script>
//问题1:返回唯一对象
const obj={count :0}
export default{
data(){
return obj
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>代理的是同一个对象,这导致一个组件实例改变数据,其他组件实例跟着改变,开发中要避免这种写法
深层对象&浅层对象
- 深层对象指的是一个对象里有多层对象(对象套娃)
- data返回的数据都会被vue代理,无论该数据处于第几层
<script>
import Button from './components/Button.vue'
import {shallowReactive} from"vue"
export default{
data(){
this.name="lusiyan"//非响应式数据
console.log(this)
window.vm=this
return shallowReactive({
msg:"hi vue",
stu:{
name:"啊伟",
age:"20",
gender:"男",
friend:{
name:"啊强",
age:"28"
}
}
})
},
components:{
Button
}
}
</script>- shallowReactive使得data返回的对象变为浅层对象,嵌套对象的属性不再是响应式数据,只有表面的msg,stu才是
- 动态的响应式数据
- 上述vm.$data这种方式不被我们所鼓励使用,原因是导致数据分布杂乱,不利于代码维护
- 我们可以直接在return{}对象里声明待响应的数据,在响应事件触发再对该数据进行赋值
- data返回的对象属性是数组
- 由于vue3底层使用代理,可以直接在模板用索引引用数组中的数据
method
- 用来指定实例对象中的方法
- 它是一个对象,可以在它里边定义多个方法
- 这些方法最终将会被挂载到组件实例上
- 可以直接通过组件实例来调用这些方法,所有组件实例上的属性都可以在模板中直接访问
- methods中函数的this会被自动绑定为组件实例,箭头函数与data同理
<script>
export default{
data(){
window.vm=this
return{
msg:"hhhhhhh"
}
},
methods:{
test(){
alert("哈哈哈哈哈")
},
HelloWorld(){
console.log(this)
return "hello world"
},
changeMsg(){
this.msg="嘻嘻嘻"
}
}
}
</script>
<template>
<h1>{{ msg }}</h1>
<h2>{{ HelloWorld() }}</h2>
<button @click=changeMsg>点一下</button>
</template>
computed
用来指定计算属性
定义与调用方式
computed:{
计算属性名:{
getter,
setter
}
}
//如果只有getter,则可以直接简写成属性名(){}
//getter只做读取相关的逻辑,不要去执行会产生作用的代码(例如修改)
//在开发中我们很少用setter,原因是可能让代码看起来复杂,可读性变差{{计算属性名}}//调用getter
组件实例.计算属性名=xxx//计算属性同样会挂载在组件实例上,setter在计算属性被修改时调用作用
- 相比data中生成的属性,计算属性用函数返回结果,在函数中可以写一些逻辑。
- 相比使用方法,计算属性有缓存,只在其依赖的数据发生变化时才会重新执行。methods中的方法每次组件重新渲染都会调用,他没有缓存。这样性能更好
- 证明计算属性实现缓存:控制台输入
组件实例.计算属性名
,计算属性方法不会重复调用 - 当你的属性不是直接显示,而是需要一定逻辑,就可以用计算属性
template
模板,它决定了组件最终的样子
定义模板的方式有三种:
1.在组件中通过template属性去指定
- 如果在组件中定义template,则会优先使用template作为模板,同时根元素中的所有内容,都会被替换
- 如果在组件中没有定义template,则会使用根元素的innerHTML作为模板使用
2.直接在网页的根元素中指定
如果直接将模板定义到网页中,此时模板必须符合html的规范
- 自定义组件名为
My-Button
,在html里会自动转换成小写my-button
,对我们使用组件有一定限制
- 自定义组件名为
3.组件中通过render()直接渲染
data & template
- 在模板中可以直接访问组件实例中的属性
- 在模板中可以通过 {{属性名}} 来访问到组件实例中的属性
- data中的响应式数据发生变化视图会自动更新
单文件组件
上述template是用字符串的形式编写的模板
- 这些字符串会在项目运行时,在浏览器中被编译为js的函数(性能不太好)
- 在字符串中编写代码,体验很差
为了解决这个问题,Vue为我们提供了一种单文件组件(SFC)
- 单文件组件的格式是vue(vscode需要安装插件 Vue Language Features (Volar)来辅助实现代码高亮和提示)
- vue文件用来编写单文件组件,vue文件本身并不能被浏览器所识别,所以它必须要被构建工具打包后,才可使用
- 同时vue文件在打包时,构建工具会直接将template转换为函数,无需再浏览器中在去编译,这样一来性能也会有所提升
使用
安装包扩展vite功能
npm install -D @vitejs/plugin-vue
修改配置文件
import vue from "@vitejs/plugin-vue"
export default {
plugins: [vue()]
}创建单文件组件
.vue
- js代码写进
<script></script>
- 模板代码写进
<template></template>
- js代码写进
示例
<script>
import MyButton from "./components/Button.vue";
export default {
data(){
return {
message:"hi vue3"
}
},
components:{
MyButton
}
}
</script>
<template>
<h1>{{ message }}</h1>
<MyButton></MyButton>
<MyButton></MyButton>
<MyButton></MyButton>
</template>
响应式原理-代理
直接修改对象的属性,那么就是仅仅修改了属性,没有去做其他的事情,这种操作只会影响对象自身,不会导致元素的重新的渲染
我们希望在修改一个属性的同时,可以进行一些其他的操作,比如触发元素重新渲染。
要实现这个目的,必须要对对象进行改造,vue3中使用的是的代理模式来完成对象的改造
生成代理对象的示例:
const obj={
name:"啊伟",
age:20
}
//用来指定代理的行为
const handler={
get(target, prop, receiver){
//返回值之前做一些其他的操作...
return target[prop]
},
set(target, prop, value, receiver){
target[prop]=value
//值修改之后做一些其他的操作
}
}
//创建代理
const proxy=new Proxy(obj,handler)
console.log(proxy.name)//啊伟
proxy.name="阿强"
console.log(obj.name)//啊强使用代理对象读取属性,会调用handler中的get方法
- get用来指定读取数据时的行为,它的返回值就是最终读取到的值
- target:被代理的对象
- prop:读取的属性
- receiver:代理对象
使用代理对象修改属性,会调用handler中的set方法
- target:被代理的对象
- prop:修改的属性
- value:值
- receiver:代理对象
代理对象的意义:可以在不会对原对象产生影响的前提下,拓展对象的读取功能,例如自动渲染
vue的代理原理
- 在vue中,data()返回的对象会被vue所代理
- vue代理后,当我们通过代理去读取属性时,返回值之前,它会先做一个跟踪的操作
- 源码做了一个
track() //追踪谁用了我这个属性,并记录下来
- 源码做了一个
- 当我们通过代理去修改属性时,修改后,会通知之前所有用到该值的位置进行更新
- 源码做了一个一个
trigger() //触发所有的使用该值的位置进行更新
- 源码做了一个一个
组合式API
上述语法是选项式的写法,在开发中大多用组合式写法
相比之前的写法:
- 对象有语法限制,只能写属性和方法,函数什么代码都能写
- 对象没有作用域,函数有作用域,局部变量对其他地方无任何影响
定义变量
- 在组合式api中直接声明的变量,就是一个普通的变量,不是响应式属性,修改这些属性时,不会在视图中产生效果
- reactive()来创建一个响应式的对象
返回值
- 通过返回值来指定那些内容要暴露给外部。暴露后的内容,可以在模板中直接使用
组合式API可以在模板中直接写子组件,无需注册
代码
<script>
import { reactive } from 'vue'
export default{
setup(){
let msg="组合式api"
const countB=reactive({count:0})
function clickB(){
countB.count+=1
}
return{
mssge,
clickB,
countB
}
}
}
</script>
<template>
<button @click=clickB>点我一下</button>--点了{{ countB.count }}
<h1>{{ msg }}</h1>
</template>><script setup>
import { reactive } from 'vue';
let msg="简写组合式api"
const stu=reactive({
name:"阿伟",
age:18
})
</script>
<template>
<h1>{{ msg }}</h1>
<h2>{{ stu.name }}--{{ stu.age }}</h2>
</template>
响应式代理
reactive()
- 返回一个对象的响应式代理
- 返回的是一个深层响应式对象
- 也可以使用shallowReactive()创建一个浅层响应式对象
- 缺点:只能返回对象的响应式代理!不能处理原始值
ref()
- 接收一个任意值,把它转换为对象,并返回对象的响应式代理
- 改变量只会影响到变量自己(给变量重新赋一个地址值),在js中,无法实现对一个变量的代理
- ref在生成响应式代理时,它是将值包装为了一个对象
0 --> {value:0}
- 访问ref对象时,必须通过 对象.value 来访问其中的值
- 在模板中,ref对象会被自动解包(要求ref对象必须是顶层对象)
<script setup>
import { reactive,ref,shallowReactive } from 'vue';
const stu=reactive(shallowReactive({
name:"阿伟",
age:18,
friend:{
name:"阿强",
age:25
}
}))
let count=ref(0)
function clickCount(){
count.value++
}
</script>
<template>
<h1>{{ msg }}</h1>
<button @click=clickCount>点我一下</button>--{{ count }}
<h2>{{ stu.friend.name }}</h2>
</template><script setup>
import { ref } from 'vue';
let msg="组合式api补充"
const obj=ref({
name:"阿伟",
age:18
})//{value:{name:"阿伟",age:18}}
const obj1={
name:ref("啊强"),//{value:"阿强"}
age:ref(18)
}
</script>
<template>
<h1>{{ msg }}</h1>
<h2>{{ obj.name }}</h2>
<h2>{{ obj1.name }}</h2><!--"阿强",调用了对象的toString方法-->
<h2>{{ obj1.name.value }}</h2>
</template>
计算属性
<script setup>
import { computed } from 'vue';
let msg="hello vue"
const newMsg=computed(()=>{
return msg+="你好vue"
})
</script>
<template>
<h1>{{ msg }}</h1>
<h1>{{ newMsg }}</h1>
</template>
模板的语法
- 在模板中,可以直接访问到组件中声明的变量
- 除了组件中的变量外,vue也为我们提供了一些全局对象可以访问:比如:Date、Math、RegExp ...
- 受限的全局访问
- 除此之外,也可以通过app对象来向vue中添加一些全局变量
app.config.globalProperties
- 使用插值(双大括号),只能使用表达式【表达式,就是有返回值的语句】
- 插值实际上就是在修改元素的textContent,如果内容中含有标签,标签会被转义显示,不会作为标签生效
<template></template>
作为容器,会被浏览器解析隐藏,直接呈现标签内的内容
指令
指令模板中为标签设置的一些特殊属性,它可以用来设置标签如何显示内容。使用指令时,不需要通过{{}}来指定表达式
指令使用v-开头
v-text 将表达式的值作为元素的textContent插入,作用同{{}}
v-html 将表达式的值作为元素的innerHTML插入,有xss注入的风险
<div v-text="s"></div>
<div v-html="s"></div>v-bind 为标签动态地设置属性,可简写为":"
在vue中不能把插值直接设置为标签的属性
v-bind设置布尔值属性(属性名=属性值)时,若设置的变量非布尔值会自动转换,转换规则除
""==true
外与js中相同v-bind:属性名="变量名"
v-bind="对象名"//绑定多个属性
v-bind:[name]="value"//动态添加属性使用v-bind绑定class和style属性
- class和style可以和其他属性一样使用v-bind将它们和动态的字符串绑定;但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的;因此,Vue专门为class和style的v-bind用法提供了特殊的功能增强;除了字符串外,表达式的值也可以是
"对象"或"数组"
。
<!-- style:可以直接写对象,但是对象的属性名当样式属性(驼峰命名法,kebab-cased形式) -->
<button :style="{
backgroundColor: backColor,
color,
'border-radius': borRadius + 'px'
}">我是一个普通的按钮</button>- class和style可以和其他属性一样使用v-bind将它们和动态的字符串绑定;但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的;因此,Vue专门为class和style的v-bind用法提供了特殊的功能增强;除了字符串外,表达式的值也可以是
v-show=Boolean 底层通过"display"来设置一个内容是否显示
- 通过css样式,不会涉及到组件的重新渲染,切换的性能比较高
- 但是初始化对所有组件初始化(包括隐藏的组件),所有它的初始化性能差
v-if=表达式 可以根据表达式的值来决定是否显示元素
- 通过删除添加元素的方式来切换元素的显示,切换时反复地渲染组件,切换的性能比较差
- 只会初始化需要用到的组件,所以它初始化性能比较好
- 可以搭配其他指令一起使用,例如v-else,v-else-if,适用于一些更复杂的场景
- 可以配合template使用
<script setup>
import {ref} from "vue"
const isShow=ref(true)
</script>
<template>
<button @click="isShow=!isShow">切换</button>
<template v-if="isShow"><!--统一控制多行代码,经过浏览器编译后template会隐藏-->
<h1>1111</h1>
<h2>22222</h2>
<!--很多条代码-->
</template>
</template>
v-for="xxx in 数组",数组里有多少项就会遍历几次
Vue 默认按照“就地更新”的策略来更新通过
v-for
渲染的元素列表。也就是默认根据元素顺序对比新旧列表的差距来最小程度地修改DOM,如果打乱顺序,可能导致与我们要的结果不同。<script setup>
import {reactive} from"vue";
const arr=reactive([{
id:1,
name:"啊伟",
age:18
},{
id:2,
name:"啊强",
age:20
},{
id:3,
name:"啊洪",
age:16
}])
</script>
<template>
<button @click="arr.unshift({id:4,name:'啊飘',age:15})">点我</button>
<ul>
<li v-for="{name,age} in arr">{{ name }}--{{ age }} <input type="text"></li>
</ul>
</template>1 2 3
4 1 2 3
由于vue默认是根据顺序对比比较来修改DOM,它会把1变为4,2变为1,3变为2,最后添加3
由于我们只修改数组,并不改变表单,所以表单没有做DOM修改,造成错位的现象。为了解决这个错位的问题,我们可以在标签内添加key属性来唯一标识每一项。
添加后:
1 2 3
4 1 2 3
vue只会在列表最前面插入一个DOM元素
注册组件
全局注册
- 使用
app.component(name,组件)
全局注册
//main.js中
import { createApp } from 'vue'
import App from './App.vue'
// 1:引入需要被注册的组件
import Login from './components/Login.vue'
const app = createApp(App)
// 2:全局注册组件
app.component('MLogin', Login)
app.mount('#app')
- 选项式api组件无需注册,就能直接使用全局注册的组件
- 组合式api组件无需引入,就能直接使用全局注册的组件
局部注册
- 选项式api通过components选项进行注册
- 组合式api直接import相关组件,就能使用
public
- 在模板中可以直接引用public中的内容
- public里的内容会直接打包到dist目录下,在书写路径直接从"/..."开始写
样式
全局样式
在组件中写会影响到当前页面的所有组件
<style></style>
<style scoped>
:global(h1){
font-size: 6px;
}
</style>
scoped
可以为style标签添加一个scoped属性,这样样式将成为局部样式,只对当前组件生效,降低代码的耦合性
如何实现的?
当我们在组件中使用scoped样式时,vue会自动为组件中的所有元素生成一个随机的属性 形如:data-v-7a7a37b1 生成后,所有的选择器都会在最后添加一个 [data-v-7a7a37b1]
h1 -> h1[data-v-7a7a37b1]
.box1 -> .box1[data-v-7a7a37b1]注意:随机生成的属性,除了会添加到当前组件内的所有元素上,也会添加到当前组件引入的其他组件的单根元素(多根元素不会生效)上,这样设计是为了可以通过父组件来为子组件设置一些样式(透传属性)
:deep(选择器){样式}--可以给子组件单根元素下的所有符合选择器的元素绑定样式
module
自动的对模块中的类名进行hash化来确保类名的唯一性
在模板中可以通过
:class=$style.类名
使用也可以通过module的属性值来指定变量名(默认是$style)
<script setup></script>
<template>
<div :class=$style.box1></div>
</template>
<style module>
.box1{
widows: 200px;
height: 200px;
background-color: red;
}
</style>
css使用v-bind
- 单文件组件的
<style>
标签支持使用v-bind('状态')
,将CSS的值链接到动态的组件状态 - 这个语法同样也适用于
<script setup>
,且支持JavaScript表达式(需要用引号包裹起来) - 实际的值会被编译成哈希化的CSS自定义属性,因此CSS本身仍然是静态的
- 自定义属性会通过内联样式的方式应用到组件的某个元素上,并且在源值变更的时候响应式地更新
类和内联样式
<script setup>
const arr=["box1","box2","box3"]
const arr2=[{box1:true},{box2:false},{box3:true}]
const style={
color:"red",
fontSize:"18px"
}
</script>
<template>
<div :class="arr" :style="style">hello vue</div>
<div :class="arr2"></div>
</template>
<style></style>
props
组件的数据在什么时候生成?
- 应该是在组件被使用时,即生成组件实例的时候产生,这就涉及到我们要怎么从父组件向子组件传递数据
父组件可以通过props来向子组件传递数据
这种传递方式属于单向数据流,props中的数据是只读的,不可修改。
- 好处:确保数据的安全性,降低项目的复杂度
- 原则:对于这种响应式数据,对它的修改只能由唯一组件完成(即创建数据的组件)
- 只读锁定的是变量中存储的地址值,如果props的某个属性是对象,那这个对象的属性还是可以修改的
- 即使这种情况可以修改,我们还是要尽量避免修改操作,做到数据不创建就不修改。
组合式api
子组件
const props=defineProps(["属性名","属性名".............])//定义
{{props.属性名}}//使用父组件
<子组件 :属性名="变量名",属性名=字面量></子组件>
属性名规范
- 子组件定义时采用驼峰命名法"xxYyZz"
- 父组件模板内采用"xx-yy-zz"
配置
const props=defineProps({
属性名:类型,
属性名:{
type:,//指定属性类型
required:true,//必须有这个属性
default:,//设置默认值
validator(value){
return 布尔值
}//验证数据
}//配置对象
})- 类型限制并不影响取值,开发中帮助我们做类型检查让我们项目更加严谨
- 不符合以上配置并不会报错,而是报警告,便于我们对代码进行优化
- 对于子组件有定义,父组件未传值的属性,除布尔值是false外,其余类型均是undefined
动态组件
- Component
- 最终以什么标签呈现由is属性决定
<script setup>
import {ref} from"vue"
import A from "./components/A.vue"
import B from "./components/B.vue"
const isShow=ref(true)
</script>
<template>
<button @click="isShow=!isShow">切换</button>
<component :is="isShow?A:B"></component>
</template>
网页的渲染
浏览器在渲染页面时,做了那些事:
- 加载页面的html和css(源码)
- html转换为DOM,css转换为CSSOM
- 将DOM和CSSOM构建成一课渲染树
- 对渲染树进行reflow(回流、重排)(计算元素的位置)
- 对网页进行绘制repaint(重绘)
构建渲染树(Render Tree)
- 从根元素开始检查那些元素可见,以及他们的样式
- 忽略那些不可见的元素(display:none)【在网页占位置的元素应该进树】
重排、回流
- 计算渲染树中元素的大小和位置
- 当页面中的元素的大小或位置发生变化时,便会触发页面的重排(回流)
- 某个元素改变会影响其他元素,所以需要重排确定其他元素的位置
- 例如width、height、margin、font-size ......
- 注意:每次修改这类样式都会触发一次重排!所以如果分开修改多个样式会触发重排多次,而重排是非常耗费系统资源的操作(昂贵),重排次数过多后,会导致网页的显示性能变差,在开发时我们应该尽量的减少重排的次数
- 把所有需要修改的样式包装成一个类,再把类添加到元素上
- 修改元素为display:"none",做任意的修改操作,在最后再把display改回来
- 在现代的前端框架中,这些东西都已经被框架优化过了!(框架操作的是虚拟DOM,无论做了多少次修改,最终只会做一次重排)所以使用vue、react这些框架这些框架开发时,几乎不需要考虑这些问题,唯独需要注意的时,尽量减少在框架中修改原生DOM
重绘
- 绘制页面
- 当页面发生变化时,浏览器就会对页面进行重新的绘制
例子:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.box1 {
width: 200px;
height: 200px;
background-color: orange;
}
.box2 {
background-color: tomato;
}
.box3 {
width: 300px;
height: 400px;
font-size: 20px;
}
</style>
</head>
<body>
<button id="btn">点我一下</button>
<hr />
<div id="box1" class="box1"></div>
<script>
btn.onclick = () => {
// box1.classList.add("box2")
// 可以通过修改class来间接的影响样式,来减少重排的次数
// box1.style.width = "300px"
// box1.style.height = "400px"
// box1.style.fontSize = "20px"
// box1.classList.add("box3")
// box1.style.display = "none"
// box1.style.width = "300px"
// box1.style.height = "400px"
// box1.style.fontSize = "20px"
// div.style.display = "block"
}
</script>
</body>
</html>
插槽
希望在父组件中指定子组件中的内容,可以通过插槽(slot)来实现该需求
插槽入口:
<子组件>内容</子组件>
插槽出口
子组件模板内的<slot>XXX</slot> XXX为默认,当入口无值时使用
具名插槽
<slot name="xxx"></slot>
<子组件>
<template v-slot:xxx>//v-slot:可以简写为#
<template>
</子组件>默认插槽
- 直接在子组件书写的文本或标签会被当成默认插槽的内容
<子组件>内容</子组件>
相当于:
<子组件>
<template v-slot:default>
<template>
</子组件>
对应的出口:
<slot name="default"></slot>
<slot></slot>组件在哪引入就是谁的子组件,子组件可以使用父组件定义的变量
子组件可以通过属性将数据传到父组件,其"兄弟组件"就可以使用该数据
子组件:<slot name="Box" :stu="stu"></slot>
父组件:<template v-slot:Box="stu">
{{ stu }}
</template>
事件
为元素绑定事件:
- 绑定事件使用v-on指令
- v-on:事件名
- 简写为:@事件名
- 绑定事件的两种方式:
内联事件处理器(自己调用函数)
- 事件触发时,直接执行js语句
- 内联事件处理器,回调函数的参数由我们自己传递
- 在内联事件处理器中,可以使用$event来访问事件对象
foo++
foo()方法事件处理器(vue帮我们调用函数)
- 事件触发时,vue会对事件的函数进行调用
- 方法事件处理器,回调函数的参数由vue帮我们传
- 参数就是事件对象,这个事件对象就是DOM中原生的事件对象,它里边包含了事件触发时的相关信息
foo
foo.bar
vue如何区分两种处理器:
检查事件的值是否是合法的js标识符或属性访问路径
如果是,则表示它是方法事件事件处理器。
否则,表示它是内联事件处理器<script setup>
function f(text){
alert(text)
}
</script>
<template>
<div class="box1" v-on:click="f('box1')">
<div class="box2" v-on:click="f('box2')">
//使用.stop阻止事件之间的传导,底层就是event.stopPropagation()
<div class="box3" v-on:click.stop="f('box3')"></div>
</div>
</div>
</template>
事件修饰符
- 事件名.stop停止事件的传递
- 事件名.capture事件在捕获阶段触发
- 事件名.prevent取消事件默认行为
- 事件名.self只有事件由自身触发时才会有效
- 事件名.once绑定一个一次性事件
- 事件名.passive主要用于提升滚动事件的性能
事件修饰符还可以链式书写
@click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。
透传属性
给组件设置属性,会自动给该组件的根元素添加该属性
透传发生在没有被声明为props和emit的属性
透传的属性不会覆盖子组件的属性,而是整合在子组件上
自动的透传只适用于单根组件,多根组件无效
在模板中可以通过
{{$attrs}}
来访问透传过来的属性,可以手动指定透传的属性要加到哪些元素上- $开头的都是模板中的属性,在脚本中无法访问
- 要想访问attrs,需要用到钩子
const attrs=useAttrs()
console.log(attrs);关闭自动透传,手动透传仍然有效
<!--在组合式 API 你需要一个额外的<script>块来书写inheritAttrs: false选项声明来禁止-->
<script>
export default{
inheritAttrs:false
}
</script>使用自定义属性时,最好通过props来传递,使用props相对于使用透传提高了代码的可阅读性
虽然这里的 attrs 对象总是反映为最新的透传,但它并不是响应式的 (考虑到性能因素)。
双向数据绑定
- 手动给表单双向绑定
- 把表单的value和变量作绑定,当value发生变化时,变量随之一起变化,这属于单向绑定
- 当value或响应式变量任意一个发生变化,另一个也会随之变化,双向绑定
v-modle
- v-model使得我们可以快速进行双向数据绑定
- v-model等价于
@input给响应式变量赋值
+value=响应式变量
- 修饰符
- .lazy使用change代替input事件,响应式变量在表单失去焦点后才会被赋值,而不是边输边赋值。
- .trim去除前后空格
- .number将数据转换为数值,如果是非法数值,则不进行转换
emits
- 父组件可以通过
@xx-yy-zz事件名:自定义方法
,来传递自定义方法 - 子组件
const emits=defineEmits(['xxYYZZ事件名'])
emits("xxYYzZZ事件名",参数...)
依赖注入
- 如果项目由很多层的组件构成,数据的传递就会变成麻烦起来
- 通过依赖注入,可以跨域多层组件向其他组件传递数据
- 步骤:
- 1.设置依赖
- provide(key,value)
- 2.注入数据
- value=inject(key)
- 1.设置依赖
- 注意:当不同父组件设置同名依赖,那遵循就近原则
侦听器
有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。 这时候我们就需要用到侦听器来监听状态的变化
选项式
函数式侦听器
watch{
//参数1是新数据,参数2是旧数据
//监听响应式的原始值
age(newData,oldData){
console.log("新"+newData);
console.log("旧"+oldData);
},
//监听响应式对象的某个属性,采用字符串路径的形式
'stu.name'(newData,oldData){
console.log("新"+newData);
console.log("旧"+oldData);
}
}
对象式侦听器
watch: {
//侦听的状态发生变化将调用对应侦听器的handler
age: {
handler(newData, oldData) {
console.log("新" + newData);
console.log("旧" + oldData);
},
//设置
deep:true,
immediate:true,
flush:'post'
},
"stu.name": {
handler(newData, oldData) {
console.log("新" + newData);
console.log("旧" + oldData);
}
}
}
- deep
- watch是浅层,当侦听一个对象时,仅仅改变对象的某个属性不会触发回调
- 在侦听对象里配置
deep:true
获得深度侦听器 - 深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
- immediate
- 默认情况下,watch是懒执行,仅当数据源变化时,才会执行回调。
- 设置
immediate:true
,在创建侦听器时会执行handler
- flush
- 侦听器默认情况下handler中访问的DOM将是更新之前的状态
- 设置
flush:"post"
,使得访问DOM将是更新之后的值 - 原理:这样设置修改了handler的触发机制(DOM更新后)
创建侦听器
语法:通过组件实例.$watch()创建侦听器
this.$watch(data,method,object)
//data:所侦听的数据源,类型为String
//method:回调函数,参数1新值,参数2旧值
//object:配置对象,可以配置deep...那三个属性
- 停止这种方式创建的侦听器只需调用$watch()返回的函数
组合Api
watch函数
语法:watch(source,callback,options)
source:侦听的数据源,
- ref(包括计算属性),reactive对象
- 对象的某个属性(必须提供获取该属性的getter函数)
- 多个数据源组成的数组
callback:数据源改变调用的回调函数,参数1newData,参数2oldData
options:配置对象,deep...三个属性
注意:reactive是深度代理,所以对应的watch也是深层的,对象的某个属性变了会触发该对象的侦听,只不过newData===oldData
但对于reactive使用getter方式侦听该对象
()=>对象
,watch就是浅层的数组里的任一数据源发生改变,触发数组侦听
停止watch函数的侦听器,只要调用该函数返回值即可
watchEffect函数
- watchEffect(回调函数,options)
- 创建成功后回调函数立即执行一遍
- 用到哪些数据源,数据源发生变化,会重新执行回调函数
- 默认情况下,回调函数的第一次执行会在视图渲染之前执行
- 默认情况下,回调触发机制在DOM更新之前
- 设置配置对象
flush:'post'
,会先更新页面,再去回调函数 - 使用watchPostEffect(回调函数)代替
- 设置配置对象
- 停止侦听器同之前一样,调用返回值
生命周期
每个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如数据侦听,编译模板,挂载实例到DOM,以及在数据改变时更新DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。最常用的是created、mounted、updated和unmounted。
beforeCreate
- beforeCreate选项式声明周期函数
- 在组件实例初始化之前调用(props解析已解析、data和computed等选项还未处理)
- 不能访问组件的实例this及其组件中的数据源和函数等
- 不能访问组件中的视图DOM元素
- 组合式 API 中的setup()钩子会在所有选项式 API 钩子之前调用
created
- created选项式生命周期函数
- 在组件实例化成功后调用
- 可访问组件的实例this及其组件中的数据源和函数等
- 不能访问组件中的视图DOM元素
beforeMount/onBeforeMount
- beforeMount:选项式生命周期函数、onBeforeMount:组合式生命周期钩子
- 组件视图在浏览器渲染之前调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 不能访问组件视图中的DOM元素
mounted/onMounted
- mounted:选项式生命周期函数、onMounted:组合式生命周期钩子
- 组件视图在浏览器渲染之后调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 可以访问组件视图中的DOM元素
beforeUpdate/onBeforeUpdate
- beforeUpdate:选项式生命周期函数、onBeforeUpdate:组合式生命周期钩子
- 数据源发生变化时,组件视图重新渲染之前调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 可以访问该组件中在更新之前的DOM元素,但是不能访问该组件中在更新之后的DOM元素
updated/onUpdated
- updated:选项式生命周期函数、onUpdated:组合式生命周期钩子
- 数据源发生变化时,组件视图重新渲染之后调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 不可以访问该组件中在更新之前的DOM元素,但是可以访问该组件中在更新之后的DOM元素
beforeUnmount/onBeforeUnmount
- beforeUnmount:选项式生命周期函数、onBeforeUnmount:组合式生命周期钩子
- 组件实例被卸载之前调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 可以访问组件视图中的DOM元素
unmounted/onUnmounted
- unmounted:选项式生命周期函数、onUnmounted:组合式生命周期钩子
- 组件实例被卸载之后调用
- 可访问组件实例东西(数据源、函数、计算属性等)
- 不可以访问组件视图中的DOM元素
- 一般在这个生命周期函数里,我们可以手动清理一些副作用,例如计时器、DOM事件监听器或者与服务器的连接
模板引用
如果我们需要直接访问组件中的底层DOM元素,可使用vue提供特殊的ref属性来访问
访问模板引用
- 在视图元素中采用ref属性来设置需要访问的DOM元素
- 该ref属性可采用字符值的执行设置
- 该ref属性可采用
v-bind:
或:ref
的形式来绑定函数,其函数的第一个参数则为该DOM元素
- 如果元素的ref属性值采用的是字符串形式
- 在选项式 API JS中,可通过
this.$refs
来访问模板引用 - 在组合式 API JS中,我们需要声明一个同名的ref变量,来获得该模板的引用
- 在选项式 API JS中,可通过
<script setup>
import {ref,onUpdated} from "vue"
const name=ref(null)
const value=ref("小明")
onUpdated(()=>{
console.log(name.value);
})
const getAgeDom=(dom)=>{
console.log(dom);
}
</script>
<template>
<div>姓名:<input type="text" ref="name" v-model="value"></div>
<div>年龄:<input type="text" :ref="getAgeDom" name="age"></div>
</template>
<style scoped></style>
v-for中的模板引用
当在v-for中使用模板引用时:
- 如果ref值是字符串形式,在元素被渲染后包含对应整个列表的所有元素【数组】
- 如果ref值是函数形式,则会每渲染一个列表元素则会执行对应的函数【不推荐使用】
<script setup>
import { onMounted, ref } from 'vue';
// 书本
let books = ref([
{ id: 1, name: '海底两万里' },
{ id: 2, name: '骆驼祥子' },
{ id: 3, name: '老人与海' },
{ id: 4, name: '安徒生童话' },
])
let bookList = ref(null)
onMounted(() => {
console.log(bookList.value); // 获取引用的 DOM 对象,并打印,发现是数组,
})
</script>
<template>
<ul>
<li v-for="b in books" :key="b.id" ref="bookList">
{{ b.name }}
</li>
</ul>
</template>
组件上的ref
模板引用也可以被用在一个子组件上;这种情况下引用中获得的值是组件实例
- 如果子组件使用的是选项式 API ,默认情况下父组件可以随意访问该子组件的数据和函数,除非在子组件使用expose选项来暴露特定的数据或函数,expose值为字符串数组
- 如果子组件使用的是组合式 API
<script setup>
,那么该子组件默认是私有的,则父组件无法访问该子组件,除非子组件在其中通过defineExpose({数据,函数....})
状态管理
- 状态(state)就是能够跟用户交流的数据
- 视图(view)用来呈现数据,用户通过视图访问数据
- 交互(actions)用户的操作,状态会根据用户在视图中的操作发生变化
状态的使用
- state提升:把state提升到共同的父组件里,再通过依赖注入使用
- 创建状态store
- src新建一个stroe文件夹
- 新建js文件存储state
- 其他组件要使用该state只需引入该js文件
- 不足:手动创建,没有代码规范
- 使用pinia
pinia
Pina是Vue专属状态管理库,它允许你跨组件或页面共享状态
vue中的状态存储仓库
引入pinia
npm install pinia
- main.js:
import { createPinia } from 'pinia'
- 创建pinia实例:
const pinia = createPinia()
- 将pinia作为中间件引入
app.use(pinia)
利用pinia创建store
- store它有三个概念,state、getters和actions,我们可以l理解成组件中的data、computed和methods
- 在项目中的src\store文件夹下不同的store.js文件
- store是用defineStore(name, function | options)定义的,建议其函数返回的值命名为use...Store方便理解
选项式
import { defineStore } from "pinia";
//defineStore需要两个参数:id,options
export const useCountStore=defineStore("count",{
//state,是一个函数
state:()=>{
//对象的数据就是store存储的数据
return {
count:0
}
},
//基于state的计算属性
getters:{
doubleCount: (state) => state.count * 2,
avgCount(){
return this.count/2
}
},
//方法,对state数据进行一些操作
actions:{
addCount(){
this.count++
}
}
})
- state中的属性和方法都可以使用store实例对象直接访问
- store实例本身就是一个reactive对象,如果将state中的数据直接解构出来,那么得到的数据将失去响应性
- 可以通过
const {...}=storeToRefs(store实例)
来对store进行解构- 它可以将state中的属性和方法以及计算属性(无法解构actions中方法)解构为ref属性,从而保留其响应性
组合式
import { defineStore } from "pinia";
import { computed, ref } from "vue";
//defineStore需要两个参数:id,options
export const useCountStore=defineStore("count",()=>{
const count=ref(0)
function addCount(){
count.value++
}
const doubleCount=computed(()=>{
return count.value*2
})
return {count,addCount,doubleCount}
})
- ref() 就是 state 属性
- computed() 就是 getters
- function() 就是 actions
store
修改state:
- 直接修改
- 通过$patch({新的state对象})
- 对于引用类型不太友好
- 通过$patch((state)=>{})
- 直接替换state,$state={...}
- 重置state,$reset(),使得state重置为初始值
订阅state
- 当store中的state发生变化时,会调用订阅里的回调函数参数
//mutation表示修改的信息
//mutation.type 修改方式 'direct' | 'patch object' | 'patch function'
//mutation.storeId // 在定义store时传入的那个id
//mutation.payload // 传递给 store实例对象.$patch() 的参数对象。
//(只有 mutation.type === 'patch object'的情况下才可用)
store实例对象.$subscribe((mutation,state)=>{},options)- 默认情况含有订阅代码的组件一卸载,那么订阅的效果会随之消失
- 设置options
{detached:true}
,组件卸载,订阅的效果仍会生效 - 使用订阅时,不要在回调函数中直接修改state,会触发递归
订阅action
- 监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。
store实例对象.$onAction(({...})=>{})
- onAction会自动帮我们传进一个参数对象{...}
- name:调用的action的名字
- store:store实例
- args:action接收的参数
- after():可以设置一个回调函数,函数会在action成功调用后触发
- onError():可以设置一个回调函数,函数会在action调用失败后触发
- 回调函数帮我们传递err,传递失败信息