4.2 基础概念
前面我们基本认识了 Vue3,并搭建了本地开发环境。现在我们从基础概念和 API 入手,完整的学习 Vue3 框架。
4.2.1 状态与方法
Vue 基于数据驱动视图的思想实现了框架运行的基本逻辑。能够驱动视图变化的数据就被称为状态。
在 Vue 中,状态被定义在 data() 方法返回的对象中。常见的用法是在单文件组件内定义状态,此时的状态只对该组件有效,因此也被称为局部状态。局部状态在组件初始化时被创建,并将状态挂在到组件实例上,因此状态可以在组件实例中通过 this 访问。
export default {
data() {
return {
count: 1,
tag: "数量",
};
},
created() {
console.log(this.tag, this.count); // 数量 1
},
};
代码中的 created() 方法会在组件初始化后执行(后面会介绍)。我们看到在这个方法内通过 this 访问到了状态的值。
状态可以直接被绑定到模版中,在模版中不需要使用 this,直接绑定状态名即可:
<templete>
<h2>{{ tag }}</h2>
<h3>{{ count }}</h3>
</templete>
当修改状态时,通常是通过 methods 属性定义方法,该属性下包含组件的所有方法:
export default {
data() {
return {
count: 1,
tag: "数量",
};
},
methods: {
changeCount() {
this.count = 2;
},
changeTag() {
this.tag = "长度";
},
},
};
与状态一样,方法也通过 this 访问。方法是修改状态的媒介,定义好方法之后,可以在实例中调用,也可在模版中调用:
export default {
methods: {
changeCount() {
this.count = 2;
},
change() {
this.changeCount();
},
},
created() {
this.changeCount();
},
};
<template>
<button @click="changeCount">修改数量</button>
</template>
但是要注意:Vue 将 methods 下所有方法内的 this 都指向了组件实例,以此确保 this 在任意情况下指向正确。因此在 methods 中定义方法不可以使用箭头函数,因为箭头函数的 this 会指向父元素,这可能会导致指向错误。
// 错误示例
export default {
methods: {
change: () => {
this.count = 2;
},
},
};
4.2.2 条件与列表
在模版的动态渲染中,常常用到条件渲染和列表渲染,这里会涉及到 Vue 的另一个知识点 ——— 指令。
在 Vue 模版中,以 “v-” 开头的属性被称为指令,可以说指令是一种特殊属性,被用于控制如何渲染元素。Vue 提供了许多内置指令来实现不同的功能。
条件渲染
内置指令 v-if 用于按照条件渲染一块内容。条件可以是一个状态,也可以是一个表达式,当值为 true 时内容被渲染,否则不渲染。
<span v-if="bool">A</span>
<span v-if="tag == 'ok'">B</span>
data() { return { bool: true, tag: 'ok' } }
与 v-if 指令搭配使用的还有 v-else、v-else-if。其实这些指令都对应着 JavaScript 中的 if/else 表达式,举例如下:
<span v-if="status == 1">已成功</span>
<span v-else-if="status == 0">未成功</span>
<span v-else>出现异常</span>
将这些指令的逻辑用 JavaScript 表达如下:
var = status = 'xxx'
if(status==1) {
return '已成功'
} else if(status==0) {
return '未成功'
} else {
return '出现异常'
}
与 v-if 非常相似的一个指令是 v-show。v-show 的作用也是控制元素是否显示,当表达式的值为 true 时,元素会显示。如下:
<span v-show="is_show">我已显示</span>
那么 v-if 与 v-show 有什么区别呢?
v-if 的关键字是“渲染”,而 v-show 的关键字是“显示”。当 v-if 的表达式为 true 时,元素会被渲染到浏览器上;反之,元素不会被渲染。
而 v-show 不管表达式的值是什么,元素都会被渲染,浏览器中一定会存在这个元素。但 v-show 通过切换 CSS 样式中的 display 属性来控制元素的显示。
那什么时候使用 v-if,什么时候使用 v-show 呢?这里引用官网的介绍:
总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
列表渲染
内置指令 v-for 用于将一个数组渲染成元素列表。v-for 指令与 JavaScript 中的 for 循环逻辑基本一致,举例如下:
data() {
return {
lists: [
{ id: 1, name: '西瓜' },
{ id: 2, name: '香蕉' }
]
}
}
<ul>
<li v-for="(item,index) in lists">
{{index}}:{{ item.name }}
</li>
</ul>
v-for 指令的表达式格式为 “(item,index) in lists”。其中 lists 表示源数组,item 和 index 分别表示数据项和对应的索引。上述代码渲染后的结果是:
<ul>
<li>0:西瓜</li>
<li>1:香蕉</li>
</ul>
值得庆祝的是,v-for 还支持直接遍历对象。v-for 会自动调用 Object.keys() 转换对象,得到属性名和属性值。
var json = {
name: 'sofa',
seats: 5,
color: 'orange',
}
<li v-for="(value,key) in json">
{{key}}:{{ value }}
</li>
因为虚拟 DOM 的缘故,使用 v-for 时必须指定一个唯一的 key 属性来保证最高效率的更新。key 建议使用每个数组项中的唯一值,不建议使用索引。
<li v-for="(item,index) in lists" :key="item.id">
{{index}}:{{ item.name }}
</li>
4.2.3 模版语法
前面介绍了在模版中绑定数据,以及使用简单的指令。模版中还有很多语法我们一一介绍。
绑定 HTML
在模版中使用双花括号()绑定数据,这种方式只能渲染文本,不能渲染 HTML。若想插入 HTML,需要使用 v-html 指令:
return {
content: '<span>文本内容</ssan>'
}
<div>{{content}}</div> // 渲染为字符串
<div v-html="content"></div> // 渲染为DOM元素
绑定属性
模版中除了数据是动态的,我们希望属性也可以动态指定,比如 class。常见需求是动态切换 class 类名来实现 UI 变化。
使用 v-bind 指令来实现属性的动态绑定,一般使用三目运算符,比如:
<div v-bind:class="active?'menu active':'menu'">菜单</div>
因为 v-bind 非常常用,因此 Vue 提供了特定的简写语法:
// 将 “v-bind:” 简写为 “:”
<div :class="active?'menu active':'menu'">菜单</div>
特殊情况:当 v-bind 绑定的值为布尔类型时,此时会控制属性是否显示。如果值为假值(false、null、undefined),属性会被移除。
return {
disabled: false
}
<button :disabled="disabled">菜单</button>
如果元素上要绑定的属性非常多,Vue 还提供了更便捷的方法 ——— 绑定对象。将所有属性写在一个对象内,v-bind 会自动解析。
return {
attrs: {
id: 'main',
class: 'content'
}
}
<button v-bind="attrs">菜单</button>
表达式
无论是双花括号语法还是指令,Vue 都支持绑定状态或表达式。表达式发挥了 JavaScript 的能力,使绑定更加灵活。表达式有以下常见的几种形式:
三目运算符
。数据处理函数
。自定义函数
。
这几种形式必须返回一个值,否则表达式不成立。假设数据如下:
data() {
return {
msg: 'you are brave'
}
}
methods: {
showMsg(str) {
if(!str) {
return 'no msg'
} else {
return 'msg:'+str
}
}
}
三种运算符举例如下:
<div class="contanor">
<p>{{msg.length>3?'标准':'偏少'}}</p>
<p>{{msg.split(' ').join('-')}}</p>
<p>{{showMsg(msg)}}</p>
</div>
4.2.4 计算属性与监听器
Vue 中有一个非常强大的功能 ——— 计算属性。计算属性的本质是依据某个状态返回一个新值,当模版中需要使用较为复杂的表达式时,将它提取为一个计算属性非常合适。
假设现在有一个商品列表数据,我们需要展示商品列表的总价,你可能会这样:
export default {
data() {
return {
lists: [
{ name: '鸡蛋', price: 3.5 },
{ name: '西红柿', price: 4.5 },
{ name: '黄瓜', price: 6 },
]},
totalPrice: 0
}
},
methods: {
getPrice() {
this.totalPrice = this.lists.reduce((a,b)=> a+b.price, 0)
}
}
}
上面代码中,我们用状态 totalPrice 表示总价,然后在一个方法内计算总价。这样做的弊端是:当商品数据变化时,需要手动调用该函数重新计算总价。
因为总价是基于商品数据计算得来的,所以用计算属性非常合适。计算属性定义在 computed 对象下,在模版中使用计算属性和使用普通状态一致。
<div>{{totalPrice}}</div>
computed: {
totalPrice() {
return this.lists.reduce((a,b)=> a+b.price, 0)
}
}
计算属性必须有返回值,否则无效。当商品数据变化时,计算属性会自动更新。
使用计算属性时要注意以下两个问题:
- 计算属性不应有副作用(如请求 API)
- 不能直接修改计算属性的值
但如果确实要在状态变化之后执行一些“副作用”操作(请求 API),那该怎么做呢?这里就要用到另一个功能 ——— 监听器。
监听器与计算属性非常相似,以至于常常被混用,但他们的功能区分很明确。计算属性是一个纯函数,只返回一个新值;而监听器不需要有返回值,只监听数据变化并执行某些操作。
监听器定义在 watch 对象下,举例如下:
export default {
data() {
return {
name: '朱元璋'
}
}
watch: {
name(val, oldval) {
console.log(val, oldval)
fetch('https://testapi/xxx', val)
}
}
}
上面代码中定义了状态 name 并监听。当 name 修改后,监听器就会被触发。监听器函数的两个参数表示 name 修改前后的值:前者是新值,后者是旧值。在监听器内可以发起一个请求,也可以执行其他任意操作。
监听器默认是“浅层”监听。也就是说,只有状态被重新赋值时才会被监听到。如果监听一个数组,数组项被修改,监听器默认是监听不到的。
那怎么办呢?监听器提供了深层监听的方式,代码如下:
export default {
data() {
return {
lists: [
{ name: '朱元璋' },
{ name: '朱标' },
]
}
}
watch: {
lists: {
handler(val) {
console.log(val) // list 数组
},
deep: true,
}
}
}
深层监听的开销很大,因此优先使用浅层监听,只有监听复杂数据才使用深层监听。
4.2.5 事件处理
前面我们在模版中使用 @click 语法来触发一个点击事件,@ 并不是新语法,它只是指令 v-on 的简写。指令 v-on 用于监听 DOM 事件,以下两种写法是一样的:
<button @click="test">点击</button>
<button v-on:click="test">点击</button>
指令 v-on 的事件名对标 HTML 的原生事件名。规则如下:
- onclick(HTML)等于 @click(Vue)
- onchange(HTML)等于 @change(Vue)
- oninput(HTML)等于 @input(Vue)
指令 v-on 的值可以是一个简单的 JavaScript 语句,也可以是一个方法。方法可以是方法名,也可以调用方法并传参。
<button @click="fun">方法名</button>
<button @click="fun('custom')">调用方法</button>
<button @click="count++">语句</button>
methods: {
fun(e) {
if(typeof e == 'string') {
console.log(e)
} else {
console.log(e.target.tagName)
}
}
}
当 v-on 的值是方法名时,方法会默认接收一个 event 参数。该参数是原生 DOM 事件对象,可以获取到许多有用信息。比如用 event.target 获取触发该事件的元素。
事件处理中还有一些特殊操作,比如阻止事件冒泡、阻止默认事件,分别需要通过 event.stopPropagation() 方法和 event.preventDefault() 方法来实现。为了简化这些操作,Vue 提供了事件修饰符。
事件修饰符在指令 v-on 之后,用 . 表示修饰规则。代码如下:
<!-- 阻止冒泡 -->
<a @click.stop="doThis"></a>
<!-- 阻止默认事件 -->
<form @submit.prevent="onSubmit"></form>
4.2.6 表单双向绑定
表单是前端中操作最频繁,也是处理最复杂的部分。即便 Vue 提供了数据绑定,但普通的表单处理还需要事件监听配合,操作起来略显繁琐。如下:
<input :value="text" @input="e => text = e.target.value" />
上述代码中,input 元素绑定了状态 text。当用户编辑输入框内容时,为了保证状态与视图同步,还需要监听用户输入事件,将状态 text 的值手动更新为编辑后的内容。
为了简化这一操作,Vue 提供了 v-model 指令。使用 v-model 改造上述代码:
<input v-model="text" />
很显然,v-model 使用更简单,并且状态会自动更新。那么 v-model 是如何实现这一功能呢?
事实上,v-model 只是一个语法糖,它依然是通过事件监听来修改状态。只不过它将数据绑定和事件监听封装在了指令内部,对外只需要绑定状态,通过这种超简单的做法实现了双向数据绑定。
指令 v-model 不仅对 input 生效,对其他表单元素如 <textarea>
、<select>
同样有效。只不过不同的表单元素对应的事件可能不同,v-model 在指令内部做了兼容处理。
不同的表单元素 v-model 的处理规则如下:
<!-- 输入框 -->
<input :value="value" @input="e=> value=e.target.value" />
<input v-model="value" />
<!-- 多行输入框 -->
<textarea :value="value" @input="e=> value=e.target.value" />
<textarea v-model="value" />
<!-- 下拉框 -->
<select :value="value" @change="e=> value=e.target.value" />
<select v-model="value" />
<!-- 多选框 -->
<input type="checkbox" :checked="value" @change="e=> value=e.target.checked" />
<input type="checkbox" v-model="value" />
<!-- 单选框 -->
<input type="radio" :checked="value" @change="e=> value=e.target.checked" />
<input type="radio" v-model="value" />
4.2.7 DOM 操作
Vue 的理念是数据驱动视图,因此大多数情况下要避免直接操作 DOM。但是在某些情况下,我们仍然需要访问和操作 DOM 元素。
基于此情况,Vue 提供了 ref 属性来标记对某个元素的引用。当组件挂载后,所有标记了 ref 属性的元素都可以通过 this.$refs 找到。
<input ref="dom"/>
mounted() {
this.$refs.dom.focus()
}
上面代码中,我们为元素标记了名为 dom 的引用。当组件挂载后,即可通过 this.$refs.dom 访问到这个元素。
使用 ref 属性要注意以下两个问题,否则会引起错误:
- 只可以在组件挂载后访问元素,否则引用不存在。
- ref 属性值必须唯一,否则会导致引用覆盖。
请记住一个原则:只有某些少数的特殊场景才需要操作 DOM,绝大多数的逻辑请使用数据驱动!