Vue2篇
一、Vue2基础
1.什么是Vue
Vue是用于构建用户界面的渐进式JavaScript框架。
所谓构建用户界面就是基于数据渲染出用户所看到的页面。
2.Vue2的特点
1.采用组件化开发。
Vue2将应用拆分为多个独立、可复用的组件,每个组件专注于特定功能,从而提高了代码复用率,让代码更好维护。在后面组件化开发会详细的讲解这部分内容。
2.声明式编码。
像JavaScript则是命令式编码,例如要给一个div添加背景色,首先要获取到该div元素,然后使用元素的 style 对象访问 CSS 属性,设置其背景色backgroundColor。
<div id="myDiv">这是一个 div</div>
<button onclick="changeColor()">改变颜色</button>
<script>
function changeColor() {
const div = document.getElementById('myDiv');
div.style.backgroundColor = 'blue'; // 直接设置颜色值
}
</script>
可见,命令式编码以 “如何做” 为核心,他需要通过明确指令告诉计算机每一步操作,关注执行过程。
而声明式编码则更关注于“做什么” ,它不关心具体实现步骤,而是由框架或语言机制处理执行细节。
3.使用虚拟DOM+Diff算法。
在原生JavaScript中,它直接将数据转化为真实DOM,当添加数据或者替换到原数据时,它会重新渲染整个DOM,这就会导致大量不必要的性能开销。
而Vue框架则是将数据转化为虚拟DOM,再由虚拟DOM转为真实DOM。当添加数据或者替换到原数据时,Diff算法会将新旧虚拟DOM进行比对,相同则直接复用真实DOM,不同则再更新。这样就减少了不必要的性能开销。
关于虚拟DOM和Diff算法,有些朋友可能会好奇它们到底是什么。
虚拟DOM: 是一种轻量级的 JavaScript 对象,它是真实 DOM 的抽象表示。
Diff算法:进行精细化比对(新、旧虚拟DOM间),实现最小量更新。
右侧就是虚拟DOM,各位可以对应看看各个属性的数据,这里就不多赘述了。
3.Vue基本语法
(1)准备容器
<div id="root"> </div>
(2)引包(开发版本/生产版本)
//本地引入
<script type="text/javascript" src="./js/vue.js"></script>
//在线引入
<!-- 开发版本,包含帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
(3)创建Vue实例
new Vue({ })
(4)指定配置项
el:通过选择器指定挂载点,看他为哪个容器服务。
data:为模板提供数据。
一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新。
new Vue({
el:'#root',
data:{
username:''
}
})
对于将实例挂载到容器,除了通过el挂载,还有另一种方法:
vm.$mount('#root') //$mount是vue原型上的方法,vue实例能够使用
el会在实例创建后立即挂载到容器上,而$mount更具灵活性,能够控制挂载时机。甚至可以通过定时器让它1秒钟后再挂载。
<div id="root">{{msg}}</div>
<script>
const vm=new Vue({
data:{
msg:'aaa'
}
})
setTimeout(function(){
vm.$mount('#root')
},1000)
</script>
另外data除了可以写成“对象式”,还可写为“函数式”。
<div id="root"></div>
<script>
const vm=new Vue({
el:'#root',
data:function(){
return{
msg:'aaa'
}
}
})
//可简写为:
data(){
return{
msg:'aaa'
}
</script>
需要记住的是:
组件化开发时data必须写成函数!!
当data是普通函数时,this——>vue实例
当data是箭头函数时,由于箭头函数没有this,所以this——>window
所以data最好写为普通函数。
4.插值表达式
{{表达式}}
作用:利用表达式进行插值,渲染到页面。
插值表达式可以直接读取到data中的所有属性。
(但其实data中数据存储到vue实例上,插值表达式可以直接读取到vm上数据。)
<div id="root">{{msg}}</div>
<script>
new Vue({
data:{
msg:'aaa'
}
})
</script>
结果:
5.Vue指令
v-html
v-html的作用是将绑定表达式的值作为 HTML 内容插入到所在的 DOM 元素中,支持解析标签。双引号里不能直接放置需要显示的字符串内容。
错误示例:
<div v-html="<span>这段内容不会被正确解析</span>"></div>
正确用法:应该将 HTML 字符串定义在 Vue 实例的数据属性中,然后通过v-html绑定这个属性。
<div id="root">
<div v-html="msg"></div>
</div>
<script>
new Vue({
el:'#root',
data:{
msg:'<h1>文本</h1>'
}
})
</script>
v-text
可以向所在标签中插入文本,无法解析标签。
<div id="root">
<div v-text="msg1">初始文本</div>
</div>
<script>
new Vue({
el:'#root',
data:{
msg1:'文本'
}
})
</script>
结果:
他的效果类似于插值表达式{{msg1}},但插值表达式可以进行拼接,而v-text拿到msg1的值后会完全覆盖掉整个div的内容。
v-show
控制元素的显示、隐藏。
等价于为元素添加display:none是视觉上的隐藏,适用于需要频繁切换显示隐藏的时候。
引号内表达式为true显示,为false隐藏。
<div v-show="表达式">文本</div>
v-if
控制元素的显示、隐藏。
等价于创建、删除元素,在结构上消失。适用于不频繁显示隐藏。
引号内表达式为true显示,为false隐藏。
<div v-if="表达式">文本</div>
v-else 与 v-else-if
控辅助v-if判断渲染。必须紧跟v-if否则无效。
他会按照顺序依次执行,一旦满足某个条件,就会渲染对应元素,并且不会对后续的条件进行判断,即使下面的判断有成立的。
<div id="root">
<div v-if="num===10">文本1</div>
<div v-else>文本2</div>
</div>
<script>
new Vue({
el:'#root',
data:{
num:10
}
})
</script>
结果:
v-on 事件处理
(1)基本语法
v-on是用于监听 DOM 事件的指令,它可以绑定事件监听器到元素上。当指定的事件触发时,会执行对应的 JavaScript 代码或方法。
<!-- 内联语句 -->
<button v-on:click="count++">点击加1</button> //可直接使用data中数据
<!-- 方法调用 -->
<button v-on:click="handleClick">点击调用方法</button>
<!-- 缩写形式(@符号) -->
<button @click="handleClick">缩写语法</button>
v-on所调用的方法,在vue实例的methods中声明,最终会在vm上。并且当要使用data中的数据时,需要用this指向对应的vue实例,即:this.属性
<div id="root">
<button @click="handleClick">点击count加1</button>
<p>{{count}}</p>
</div>
<script>
new Vue({
el: '#root',
data: {
count: 1
},
methods: {
handleClick() {
this.count++
}
}
})
</script>
要注意这里的this指向并不一定是vue实例,当methods中定义的方法是箭头函数时,this指向window。(所有被vue所管理的函数最好都写成普通函数)
(2)传参
如果事件处理方法没有显式传递参数,Vue 会自动传递原生 DOM 事件对象,event。
<button @click="handleClick">点击</button>
methods: {
handleClick(event) {
console.log(event.target); // 输出触发事件的DOM元素
}
}
如果需要传递其他参数时:
<button @click="handleClick(66)">点击</button>
methods: {
handleClick(num) {
console.log(num); // 输出66
}
}
这时候会发现event没有了,就算我们再设置几个参数也没法拿到event。
<button @click="handleClick(66)">点击</button>
methods: {
handleClick(num,a,b,c) {
console.log(num,a,b,c); // 输出66,undifined,undifined,undifined
}
}
那如果既想传传参,还想要event,就可以用 $event进行占位 ,具体顺序无所谓。
<button @click="handleClick(66,$event)">点击</button>
methods: {
handleClick(num,e) {
console.log(num,e.target.innerHTML); // 输出66,点击
}
}
(3)事件修饰符
- 阻止默认行为:@click.prevent
- 阻止冒泡:@click.stop
冒泡:从目标节点—>文档根结点(子—>父)
<div class="demo1" @click="showInfo">
<button @click.stop="showInfo">点我提示信息</button> //若没阻止冒泡会弹出提示两次
</div>
- 事件只触发一次:@click.once
- 进行事件捕获:@click.capture
捕获:从根节点—>目标结点(父—>子)
<div class="demo2" @click.capture="showInfo(1)">
div1
<div @click="showInfo(2)">div2</div>
</div>
<script>
new Vue({
el: '.demo2',
methods: {
showInfo(value) {
console.log(value)
}
}
})
</script>
- 只有当前元素才能触发:@click.self
<div class="demo2" @click="showInfo">
div1
<div @click="showInfo">div2</div>
</div>
<script>
new Vue({
el: '.demo2',
methods: {
showInfo(e) {
console.log(e.target)
}
}
})
</script>
点击div2,可以发现返回的都是div2,也就是说无论冒泡到哪里,触发事件的一直都是div2。
当为div1加上self修饰符时,并点击div2时:
<div class="demo2" @click.self="showInfo">
div1
<div @click="showInfo">div2</div>
</div>
此时确实触发了div1的showInfo,但e.target却是div2,而非它本身,所以div2不会执行。
所以有时候也可以用self去阻止冒泡。
- 事件默认行为立即执行,无需等待回调函数执行完毕:@click.passive
另外,并不仅仅只可以有一个事件修饰符,当某元素想要阻止默认事件的同时还阻止冒泡,就可以写为:@click.prevent.stop=" "
(4)触发事件
- 点击事件 @click
- 键盘事件 @keyup @keydown
例如在传统javascript中确认点击的是“回车”可以用e.key===‘Enter’
而在vue中可以写为:@keyup.enter=“ ” - 鼠标事件
@mouseenter :鼠标进入元素时触发 (不冒泡)(推荐)
@mouseleave:鼠标离开元素时触发 (不冒泡)(推荐)
@mouseover:鼠标进入元素时触发 (冒泡)
@mouseout:鼠标离开元素时触发 (冒泡)
v-bind 单向数据绑定
v-bind可以动态设置标签属性。
(1)基础语法
v-bind:属性=“表达式” 或:属性=“表达式”
<!-- 完整语法 -->
<img v-bind:src="imageUrl" alt="示例图片">
<!-- 缩写语法(推荐) -->
<img :src="imageUrl" alt="示例图片">
例如:
<input type="text" :value="num">
<script>
new Vue({
el:'#root',
data:{
num:'111'
}
})
</script>
当data中数据发生变化时,输入框内容发生改变;当输入框内容改变时,data中的数据不发生改变。
这是因为v-bind是单向数据绑定,数据只能从data—>绑定元素。
(2)动态绑定样式
class="固定样式" v-bind:class="变化样式"
不变的样式写在class中,而后期需要变化的样式写在:class中。(原生class与绑定class共存)
通过动态绑定样式,就可以在不操作DOM元素的情况下就能够修改样式,方便vue管理。
1.字符串写法:
在:class中写属性,data属性中存样式。
<template>
<div :class="activeClass"></div>
</template>
<script>
export default {
data() {
return {
activeClass: 'bg-primary text-white' // 存储在 data 中的类名字符串
}
}
}
</script>
<style>
.bg-primary { background-color: blue; }
.text-white { color: white; }
</style>
2.数组写法
有多个样式可供选择,可以通过push、pop、shift操作数组,添加或删除样式。
直接在模板中绑定数组:
<template>
<div :class="[classA, classB]">示例文本</div>
</template>
<script>
export default {
data() {
return {
classA: 'text-primary',
classB: 'font-bold'
}
}
}
</script>
在data中定义样式数组,动态样式绑定数组名:
<template>
<div :class="classArr">示例文本</div>
</template>
<script>
data:{
classArr:['text-primary','font-bold']
}
</script>
3.对象写法
适用于样式个数确认、名称确认,并动态切换样式是否使用。
:class="{样式1:布尔值,样式2:布尔值}"
<template>
<div :class="{ 'active': isActive, 'text-danger': hasError }"> </div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false
}
}
}
</script>
或:
<template>
<div :class="classObject">动态样式示例</div>
</template>
<script>
export default {
data() {
return {
classObject: {
active: true,
'text-danger': false,
}
}
}
}
</script>
v-model 双向数据绑定
主要服务于表单元素(有value值),进行双向数据绑定,可以快速获取或设置表单元素内容。
当data中数据变化,视图自动更新。
当视图变化,data中数据自动更新。
写法:v-model:属性="值" v-model="值" //v-model默认绑定value
6.MVVM模型
是一种前端架构模式,在Vue最开始设计的时候,Vue的作者就参考了MVVM(Model-View-ViewModel)模型。而VUE的双向数据绑定就是利用了MVVM。
M:数据模型(Model)——>data中的数据
V:视图(View)——>模板代码
VM:视图模型(ViewModel)——>Vue实例对象
MVVM 的核心优势在于双向绑定:ViewModel作为“桥梁”,当 Model 变化时通过数据绑定 View 自动更新,View 交互时通过数据监听⑩ Model 同步修改。
7.数据代理(重点)
7.1 Object.defineProperty
学到数据代理,首先就要明白Object.defineProperty方法。
这个方法主要是用来给对象添加属性。
Object.defineProperty(对象名,'属性',{
//基础配置项
value:属性值,
enumerable:true/false, //是否可被枚举,默认false
writable:true/false, //是否可被修改,默认false
configurable:true/false //是否可被删除,默认false
})
假设要给person对象添加属性age,并设值为18。
<script>
let person={
name:'pink',
sex:'女'
}
//给person添加属性age
Object.defineProperty(person,'age',{
value:18
})
console.log(person)
</script>
结果:
有人可能说,我直接在对象中加入属性不是Object.defineProperty效果一样嘛。
<script>
let person={
name:'pink',
sex:'女',
age:18
}
console.log(person)
</script>
结果:
两种结果对比,可以发现通过Object.defineProperty添加的属性age颜色较浅,那是因为它想要告诉我们age属性是不可以被枚举的,意思就是说age属性不参与遍历的。
<script>
let person={
name:'pink',
sex:'女'
}
//给person添加属性age
Object.defineProperty(person,'age',{
value:18
})
for(let key in person){
console.log(key)
}
</script>
结果:
若是想让age属性被遍历,就需要在配置项中加入:
enumerable:true //enumerable可列举的
它可以控制属性是否可以被枚举,默认值为false,不被枚举的。
另外通过Object.defineProperty添加的属性默认无法修改、无法删除,可以通过以下配置项进行设置。
writable:true/false, //是否可被修改,默认false
configurable:true/false //是否可被删除,默认false
//控制台输入delete person.age删除age属性
除了以上基础配置项,Object.defineProperty还有一些高级配置项。
<script>
let num=100
let person={
name:'pink',
sex:'女',
age:num
}
console.log(person)
</script>
在person外面定义一个变量num,并让age的值为num的值。
结果:
可见确实让age拿到了num的值。但是当我们修改num的值,再次输出person时,却会发现age的值并没有发生变化。
这是因为在我们定义完num和person后代码已经执行完了,之后再怎么修改num都无法影响到age的值。除非你在修改完num的值后,用person.age=num再次获取到num的值才会变化。
如果我们想在num的值修改之后,age的值也跟着变化,那就要用到Object.defineProperty的高级配置项get了。
当有人读取属性,就会调用get方法。
<script>
let num=100
let person={
name:'pink',
sex:'女',
}
Object.defineProperty(person,'age',{
enumerable:true,
//当有人读取person的age属性时,get方法就会被调用,且返回值就是age的值
get:function(){
return num
}
})
console.log(person)
</script>
和get相辅相成的还有一个set方法。
当有人修改了数据,就会调用set方法。set方法的参数就是修改后的新值。
//当有人修改person的age属性时,set方法就会被调用,且会收到具体的值
set(value){
console.log('有人修改了age属性,且值为'+value)
num=value
}
7.2 数据代理
概述:数据代理是通过一个对象代理对另一个对象中属性的操作。(读/写)
数据代理的好处:简化访问,保持API一致性。
vue实例通过“数据代理”把_data中所有的属性都代理到实例vm上,使vm.name=vm._data.name。这样就让用户无需通过this._data.xxx去访问数据,而是直接用this.xxx。可以统一通过this去访问数据、方法和计算属性。
基本原理:
- 通过Object.defineProperty()把data中的所有属性添加到vm上。
- 为每一个添加到vm上的属性,都指定一个getter/setter。
- 在setter/getter内部去操作(读/写)data中对应的属性。
首先vue进行数据劫持,将data中的数据存入vue实例vm的_data上,
<script>
let obj1={x:100}
let obj2={y:50}
//我想让obj2读取到obj1的x属性,并且能够修改x属性
Object.defineProperty(obj2,'x',{
get(){
return obj1.x
},
set(value){
obj1.x=value
}
})
</script>
<div id="root">
<div>{{msg}}</div>
</div>
<script>
const vm=new Vue({
el:'#root',
data:{
msg:111
}
})
</script>
当我们在控制台输入vm时可以发现,在vue实例上有
这和之前用Objext.defineProperty()添加的属性效果类似。
当通过vm读msg时通过getter方法读取data中的msg,当通过vm改的msg时通过setter方法改data中的msg。
vue实例定义了一个_data将data中的数据存在它身上。若要访问data中的数据,可以在控制台输入:
vm._data.属性
可以发现_data中也有getter/setter方法,但这里不是数据代理,而是做了数据劫持。
8.计算属性
计算属性是基于现有数据,计算出新属性,它的结果会被缓存,只有在依赖数据变化时才会重新计算。
原理:底层借助了Object.defineProperty的getter和setter。
相较于method,computed具有缓存机制。
计算属性会作为属性最终存在vue实例上,直接读取使用即可。
如果计算属性被修改,那么必须写set方法去响应修改,且set中药引起计算时所依赖的数据发生改变。
(1)基本语法
<div>{{计算属性}}</div> //注意不要和data属性同名
computed:{
计算属性(){
return 结果 //把值返回给计算属性
}
}
注意:这种简单写法只能获取,无法修改。(有get无set)
(2)完整写法
computed:{
计算属性名:{
get(){
return 结果
},
set(修改的值){
//修改依赖数据
}
}
}
get的作用:是当有人读取计算属性时,get就会被调用,且返回值就作为计算属性的值。
get什么时候执行:1.初次读取计算属性时。2.所依赖的数据发生变化时。
set什么时候执行:当修改计算属性时。注意要在set中修改依赖数据,否则无效。
我们以拼接姓名为例,分别通过methods与computed实现。
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName">
<p>{{fullName()}}</p> //插值表达式调用method方法记得加()
</div>
<script>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
methods:{
fullName(){
return this.firstName+'-'+this.lastName
}
}
})
</script>
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName">
<p>{{fullName}}</p>
</div>
<script>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
computed:{
fullName(){
return this.firstName+'-'+this.lastName
}
}
})
</script>
可以发现methods与computed的结果完全相同,那既然效果相同又为什么要用computed呢?
那是因为computed相较于methods具有缓存特性。
假设:
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName">
<p>{{fullName}}</p> //多次调用计算属性
<p>{{fullName}}</p>
<p>{{fullName}}</p>
</div>
<script>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
computed:{
fullName(){
console.log('计算属性被调用了调用')
return this.firstName+'-'+this.lastName
}
}
})
</script>
结果:计算属性只被调用一次。
9.监视属性
监视属性(watch)是一种用于监听数据(data属性、计算属性、props)变化并执行相应操作的机制。
若监视的数据不存在,不会报错,新旧值都为undifined。
在 Vue 实例中,可以通过 watch 选项来声明监视属性,监视的属性要写成对象形式。
(1)基本语法
法一:
watch:{
监听属性:{
handler(newValue,oldValue){
//执行操作
console.log('新值为'+newValue,'旧值为'+oldValue)
}
}
}
当监听的属性发生变化时,调用handler。
如果希望在 Vue 实例初始化时立即执行一次监听器可以为监听属性添加配置项:immediate:true
简写:
当不需要深度监视,也不需要立即执行时可以用简写方式。
watch:{
监视属性(newValue,oldValue){
}
}
法二:
vm.$watch('监听属性',{
handle(newValue,oldValue){
}
})
示例:
<div id="root">
<p>{{count}}</p>
<button @click="count++">点击加一</button>
</div>
<script>
new Vue({
el:'#root',
data:{
count:1
},
watch:{
count:{
handler(newValue,oldValue){
console.log('新值为'+newValue,'旧值为'+oldValue)
}
}
}
})
</script>
(2)深度监听
当监视多级结构中某个属性的变化,需要用“对象名.属性名”指定监视属性,比方说监听person中的age属性。
data:{
msg:1,
person:{
name:'pink',
age:18
}
},
watch:{
"person.age":{
handler(newValue,oldValue){
console.log('新值为'+newValue,'旧值为'+oldValue)
}
}
}
那如果要监视整个person,person中的两个属性只要有一个发生变化就出发监听应该怎么写呢?这时候有人可能就这么写:
watch:{
person:{
handler(newValue,oldValue){
console.log('新值为'+newValue,'旧值为'+oldValue)
}
}
}
但是控制台却没有任何输出。
原因是watch确实是在监视person对象,但watch却不监视对象里面的属性,而是把对象里面的看作一个整体。无论对象中的值如何变化,对像指定的地址都不发生改变,它就认为对象没有发生变化。
除非把对象整个替换掉,监视才会生效。
正确写法:加配置项deep:true
watch:{
person:{
deep:true //深度监听
handler(newValue,oldValue){
console.log('新值为'+newValue,'旧值为'+oldValue)
}
}
}
这样name变了person监听会运行,age变了person监听也会运行。
所以:watch默认不监听多级结构中的属性,配置deep:true可以监测对像内部值变化。
10.监视属性与计算属性
computed:
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName">
<p>{{fullName}}</p>
</div>
<script>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
computed:{
fullName(){
return this.firstName+'-'+this.lastName
}
}
})
</script>
watch:
<div id="root">
<input type="text" v-model:value="firstName"><br>
<input type="text" v-model:value="lastName">
<p>{{fullName}}</p>
</div>
<script>
const vm=new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三',
fullName:''
},
watch:{
firstName(newValue){
this.fullName=newValue+'-'+this.lastName
},
lastName(newValue){
this.fullName=this.firstName+'-'+newValue
}
}
})
</script>
可以发现watch监听既需要在data中定义fullName,还需要监听firstName和lastName,相较于computed来说比较麻烦。
但当想要修改姓名后1s再更新fullName呢?
watch:
watch: {
firstName(newValue) {
setTimeout(() => {
this.fullName = newValue + '-' + this.lastName
}, 1000)
},
}
computed:
computed:{
fullName(){
setTimeout(()=>{
return this.firstName+'-'+this.lastName
},1000)
}
}
运行发现用定时器在watch里成功运行,但computed中却无效,这是因为computed不支持异步,watch支持异步任务。
为什么 computed 不支持异步?
计算属性(computed)的核心设计是同步计算,因为它必须在依赖数据变化时立即更新结果,以保证数据的响应式一致性。
如果计算属性支持异步:
异步操作的结果不会被 Vue 自动追踪为依赖。
计算属性会在异步完成前就返回 undefined,导致结果不可预测。
watch中定时器要写成箭头函数。
如果是普通函数,this—>window
如果是箭头函数,本身无this,需要向外层寻找,这里定时器的this等同于firstName,而firstName属于vue实例。
*vue中关于要写成箭头函数还是普通函数:
1.被vue所管理的函数,最好写成普通函数,这样this的指向才是vue实例。
2.所有不被vue所管理的函数(如回调函数),最好写成箭头函数,这样this的指向才是vue实例。