Vue2基础笔记回顾(二)
Vue 数组的变异方法
目标
了解 v-for 更新的机制。
概念:所谓数组变异方法就是数组数据的变化会导致视图更新的方法,常见的有如下几个。
push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
。
非变异方法有这几个:slice()
、filter()
、concat()
。
<template>
<div>
<ul>
<li v-for="(val, index) in arr" :key="index">{{ val }}</li>
</ul>
<button @click="revBtn">数组翻转</button>
<button @click="sliceBtn">截取前3个</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: [5, 3, 9, 2, 1],
}
},
methods: {
revBtn() {
// 使用变异方法 reverse
this.arr.reverse()
},
sliceBtn() {
// slice 是非变异方法,可以把处理后的结果重新赋值给 data 中的数据
let newArr = this.arr.slice(0, 3)
this.arr = newArr
},
},
}
</script>
两种非响应式的操作
所谓非响应式,就是数据命名变了,但是视图没有更新,常见的有下面两种情况。
- 通过索引去修改数组中的元素。
- 给对象后续新增的 key 进行赋值。
- 解决
this.$set(target, key, value)
或Vue.set(target, key, value)
。
Vue 基础虚拟 DOM
虚拟 DOM 的概念。
- 什么是虚拟 DOM:用 JS 对象来模拟真实 DOM(虚拟 DOM 的本质就是一个 JS 对象)。
- 为什么要有虚拟 DOM:真实 DOM 会有上百个属性,绝大多是我们不需要关心的,都对比起来会造成性能浪费。
- 虚拟 DOM 的好处:虚拟 DOM 是用 JS 对象对真实 DOM 的模拟,只包含了必要的属性,方便对比差异,提高了更新页面的性能。
- Vue 模板和虚拟 DOM 的关系:
.vue
文件中的 template 里写的标签我们称为模板,最终都要被 Vue 处理成虚拟 DOM 对象,再生成真实 DOM 片段,才会渲染显示到真实页面上。
- template 结构。
<template>
<div id="box">
<p class="my_p">123</p>
</div>
</template>
- 对应的虚拟 DOM 结构。
const dom = {
type: 'div',
attributes: [{ id: 'box' }],
children: {
type: 'p',
attributes: [{ class: 'my_p' }],
text: '123',
},
}
-
以后 Vue 数据更新。
- 生成新的虚拟 DOM 结构。
- 和旧的虚拟 DOM 结构对比。
- 找不同,只更新变化的部分到页面,这个操作也叫打补丁。
Vue 基础 key 作用
key 不能是索引
演示下面代码的问题。
<template>
<div>
<ul id="myUL">
<li v-for="(str, index) in arr" :key="index">
{{ str }}
<input type="text" />
</li>
</ul>
<button @click="addFn">下标为1的位置新增一个</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: ['老大', '老二', '老三'],
}
},
methods: {
addFn() {
this.arr.splice(1, 0, '新来的')
},
},
}
</script>
分析代码的执行过程。
1. 【初始数据】 => ['老大', '老二', '老三']
2. 根据数组 arr 在内存中生成虚拟 DOM(为了方便观察,这儿直接写成了标签,其实应该是对象)
<li :key="0"> 老大 <input type="text" /> </li>
<li :key="1"> 老二 <input type="text" /> </li>
<li :key="2"> 老三 <input type="text" /> </li>
3. 将内存中的虚拟 DOM 转成真实 DOM 并渲染到页面
4. 用户输入 1、2、3
1. 【新数据】 => ['老大', '新来的', '老二', '老三']
2. 根据数组 arr 在内存中生成新的虚拟 DOM
<li :key="0"> 老大 <input type="text" /> </li>
<li :key="1"> 新来的 <input type="text" /> </li>
<li :key="2"> 老二 <input type="text" /> </li>
<li :key="3"> 老三 <input type="text" /> </li>
3. 将内存中新的虚拟 DOM 和旧的虚拟 DOM 进行比较(VNode Diff)
4. 取出 key 0
看一下旧的虚拟 DOM 中有没有(找到了就对比标签名及标签内容)
发现有
发现标签名 li 和内部的内容一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 1 还会在那放着)
5. 取出 key 1
看一下旧的虚拟 DOM 中有没有
发现有
发现标签名 li 一样则直接进行复用
发现 li 内部的文本不一样则更新
发现 li 内部的 input 标签一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 2 还会在那放着)
6. 取出 key 2
看一下旧的虚拟 DOM 中有没有
发现有
发现标签名 li 一样则直接进行复用
发现 li 内部的文本不一样则更新
发现 li 内部的 input 标签一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 2 还会在那放着)
7. 取出 key 3
看一下旧的虚拟 DOM 中有没有
发现没有
根据此虚拟 DOM 重新创建真实 DOM 并渲染
-
理想情况:只创建一个新增的 li 节点是性能最好的。
-
实际情况:更新了两个 li 中的内容,创建创建了一个新的 li 节点。
-
结论总结:不建议用 index 当做 key,为什么?
- 可能会带来不必要的 Diff,性能浪费(例如从列表顶部或中间删除、添加元素)。
- 当页面存在 input 框时,界面可能会发生错乱。
key 不写可以吗?
key 不写,默认也是索引,同上。
key 应该是什么?
应该是唯一不重复的 ID,一般是后端返回的。 (当页面仅仅是作为列表展示时,不存在往列表的非最后添加、删除等操作,也可以使用 index)。
1. 【初始数据】 => ['老大', '老二', '老三']
2. 根据数组 arr 在内存中生成虚拟 DOM
<li :key="老大"> 老大 <input type="text" /> </li>
<li :key="老二"> 老二 <input type="text" /> </li>
<li :key="老三"> 老三 <input type="text" /> </li>
3. 将内存中的虚拟 DOM 转成真实 DOM 并渲染到页面
4. 用户输入 1、2、3
1. 【新数据】 => ['老大', '新来的', '老二', '老三']
2. 根据数组 arr 在内存中生成新的虚拟 DOM
<li :key="老大"> 老大 <input type="text" /> </li>
<li :key="新来的"> 新来的 <input type="text" /> </li>
<li :key="老二"> 老二 <input type="text" /> </li>
<li :key="老三"> 老三 <input type="text" /> </li>
3. 将内存中新的虚拟 DOM 和旧的虚拟 DOM 进行比较(VNode Diff)
4. 取出 key “老大”
看一下旧的虚拟 DOM 中有没有(找到了就对比标签名及标签内容)
发现有
发现标签名 li 和内部的内容一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 1 还会在那放着)
5. 取出 key “新来的”
看一下旧的虚拟 DOM 中有没有
发现没有
根据此虚拟 DOM 重新创建真实 DOM 并渲染
6. 取出 key “老二”
看一下旧的虚拟 DOM 中有没有
发现有
发现标签名 li 和内部的内容一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 2 还会在那放着)
7. 取出 key “老三”
看一下旧的虚拟 DOM 中有没有
发现有
发现标签名 li 和内部的内容一样则直接进行复用(一动不动,就直接进行复用渲染完毕的真实 DOM,之前输入的 3 还会在那放着)
key 作用是什么
总结:key 是用来标记虚拟 DOM 的,当数据变化后,Vue 会根据变化后的数据生成新的虚拟 DOM,DOM Diff 的时候能根据 key 快速找到旧的虚拟 DOM,然后用新的虚拟 DOM 和旧虚拟 DOM 进行比较,如果旧的虚拟 DOM 中找到了与新的虚拟 DOM 相同的 key。
- 节点及内容没变化直接复用原来的真实 DOM。
- 节点或内容变了,新的虚拟 DOM 会转成真实 DOM 并更新对应的页面部分。
Vue 基础 动态 class
用 v-bind 给标签 class 设置动态的值。
学习
- 语法规则:
:class="{类名: 布尔值}"
。 - 使用场景:Vue 变量控制标签是否应该有类名。
<template>
<div>
<p :class="{ red_str: bool }">动态 class</p>
</div>
</template>
<script>
export default {
data() {
return {
bool: true,
}
},
}
</script>
<style scoped>
.red_str {
color: red;
}
</style>
Vue 基础 动态 style
给标签动态设置 style 的值。
学习
语法规则::style="{css 属性: 值}"
。
<template>
<div>
<p :style="{ backgroundColor: colorStr }">动态style</p>
</div>
</template>
<script>
export default {
data() {
return {
colorStr: 'red',
}
},
}
</script>
案例 品牌管理
准备结构
-
需求:把默认数据显示到表格上;资产超过 100 的,都用红色字体标记出来。
-
步骤分析。
- 复制数据和标签模板,铺设静态页面。
- 此案例使用 bootstrap,需要下载,并导入到入口文件
main.js
中。 - 用 v-for 配合默认数据,把数据默认铺设到表格上显示。
- 大于 100 价格,动态绑定 class 设置 red 类名。
步骤
- 安装 bootstrap。
yarn add bootstrap
- 引入 bootstrap,
main.js
。
import 'bootstrap/dist/css/bootstrap.css'
- 模板代码(直接复制)。
<template>
<div id="app">
<div class="container">
<!-- 顶部框模块 -->
<div class="form-group">
<div class="input-group">
<h4>品牌管理</h4>
</div>
</div>
<!-- 数据表格 -->
<table class="table table-bordered table-hover mt-2">
<thead>
<tr>
<th>编号</th>
<th>资产名称</th>
<th>价格</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
<!-- 如果价格超过100,就有red这个类 -->
<td class="red"></td>
<td></td>
<td><a href="#">删除</a></td>
</tr>
</tbody>
<!--
<tfoot >
<tr>
<td colspan="5" style="text-align: center">暂无数据</td>
</tr>
</tfoot>
-->
</table>
<!-- 添加资产 -->
<form class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" placeholder="资产名称" />
</div>
</div>
<br />
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" placeholder="价格" />
</div>
</div>
<br />
<!-- 阻止表单提交 -->
<button class="btn btn-primary">添加资产</button>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
name: '', // 名称
price: 0, // 价格
list: [
{ id: 100, name: '外套', price: 199, time: new Date('2010-08-12') },
{ id: 101, name: '裤子', price: 34, time: new Date('2013-09-01') },
{ id: 102, name: '鞋', price: 25.4, time: new Date('2018-11-22') },
{ id: 103, name: '头发', price: 19900, time: new Date('2020-12-12') },
],
}
},
}
</script>
<style>
.red {
color: red;
}
</style>
- 正确代码。
<tbody>
<tr v-for="obj in list" :key="obj.id">
<td>{{ obj.id }}</td>
<td>{{ obj.name }}</td>
<!-- 如果价格超过 100,就有 red 这个类,注意不要忘了 class 前面的冒号 -->
<td :class="{red: obj.price > 100}">{{ obj.price }}</td>
<td>{{ obj.time }}</td>
<td><a href="#">删除</a></td>
</tr>
</tbody>
新增品牌
-
需求:实现把表单数据添加进表格功能;用户输入为空给出提示。
-
步骤分析。
- 通过 v-model 指令收集 name 和 price。
- 给添加资产按钮,绑定点击事件(注意不要忘记阻止默认行为)。
- 在事件的回调中,判断用户内容是否符合规定。
- 组装成新对象数据,添加数据到数组中。
代码
正确代码。
<!-- 添加资产 -->
<form class="form-inline">
<div class="form-group">
<div class="input-group">
<!-- #1 通过 v-model 指令收集 name 和 price -->
<input type="text" class="form-control" placeholder="资产名称" v-model="name" />
</div>
</div>
<br />
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" placeholder="价格" v-model.number="price" />
</div>
</div>
<br />
<!-- #2 给添加资产按钮,绑定点击事件(注意不要忘记阻止默认行为) -->
<button class="btn btn-primary" @click.prevent="addFn">添加资产</button>
</form>
<script>
export default {
// ...省略其他
methods: {
addFn() {
// #3 在事件的回调中,判断用户内容是否符合规定
if (this.name.trim().length === 0 || this.price === 0) {
alert('不能为空')
return
}
// #4 组装成新对象数据,添加数据到数组中
this.list.push({
// 当前数组最后一个对象的 id+1 作为新对象 id 值
id: this.list[this.list.length - 1].id + 1,
name: this.name,
price: this.price,
time: new Date(),
})
},
},
}
</script>
删除品牌
-
需求:点击删除的 a 标签,删除数据;没数据了要提示暂无数据的 tfoot。
-
步骤分析。
- a 标签绑定点击事件并传递 id,
<td><a href="#" @click.prevent="delFn(obj.id)">删除</a></td>
。 - 在事件回调中,根据 id 找索引,根据索引去删除。
- 删除光了要让 tfoot 显示。
- 解决删光了再新增的 Bug,
{id: this.list.length === 0 ? 0 : this.list[this.list.length - 1].id + 1}
。
- a 标签绑定点击事件并传递 id,
代码
<script>
export default {
// ...其他代码
methods: {
// ...其他代码
delFn(id) {
// 通过 id 找到这条数据在数组中下标
const index = this.list.findIndex((obj) => obj.id === id)
this.list.splice(index, 1)
},
},
}
</script>
时间格式化
复制上个案例,在此基础上,把表格里的时间用过滤器+moment 模块,格式化成 YYYY-MM-DD 格式。
实现
- 下载 moment 处理日期的第三方工具模块。
yarn add moment
- 定义方法,接收时间,返回经过 moment 处理后的格式。
// #1 在 methods 中定义方法
formatDate (val){
return moment(val).format('YYYY-MM-DD')
}
// #2 在模板中使用方法
<td>{{ formatDate(obj.time)}}</td>
Vue 计算属性 computed
计算属性的基本使用。
学习
概念:依赖 data 中的数据而产生的结果,语法如下。
{
computed: {
"计算属性名" () {
return "值"
}
}
}
需求:两数之和展示到页面上。
<template>
<div>
<p>{{ num }}</p>
</div>
</template>
<script>
export default {
data() {
return {
a: 10,
b: 20,
}
},
computed: {
num() {
return this.a + this.b
},
},
}
</script>
🤔 注意:计算属性也是 Vue 数据变量,所以不要和 data 里的变量/数据重名。
Vue 计算属性和方法
内容
- 计算属性是基于它们的依赖(data 中的数据)进行缓存,只要依赖项不变,都是直接从缓存取结果,性能高。
- 而方法都要每次重新调用/执行才能拿到处理后的结果。
<template>
<div>
<p>{{ reverseMessage }}</p>
<p>{{ reverseMessage }}</p>
<p>{{ reverseMessage }}</p>
<p>{{ getMessage() }}</p>
<p>{{ getMessage() }}</p>
<p>{{ getMessage() }}</p>
</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello, Vue',
}
},
computed: {
reverseMessage() {
console.log('计算属性执行了')
return this.msg.split('').reverse().join('')
},
},
methods: {
getMessage() {
console.log('函数执行了')
return this.msg.split('').reverse().join('')
},
},
}
</script>
案例-品牌管理(总价和均价)
基于之前的案例,完成总价和均价的计算效果。
<!-- #1 末尾补充显示总价和均价的 tr -->
<tr style="background-color: #EEE">
<td>统计:</td>
<td colspan="2">总价钱为: {{ allPrice }}</td>
<td colspan="2">平均价: {{ avgPrice }}</td>
</tr>
<script>
export default {
// ...源代码省略
// #2 定义计算属性
computed: {
allPrice() {
// #3 求总价
return this.list.reduce((acc, cur) => acc + cur.price, 0)
},
avgPrice() {
// #4 求均价 - 保留 2 位小数
return (this.allPrice / this.list.length).toFixed(2)
},
},
}
</script>
Vue 计算属性 完整写法
计算属性本质上也是变量,如果想要直接赋值,需要使用完整写法。
学习
语法如下。
{
computed: {
"属性名": {
set(值){
// 设置数据会触发这儿
},
get() {
// 获取数据会触发这儿
return "值"
}
}
}
}
需求:计算属性给 v-model 使用。
<template>
<div>
<p>firstName: {{ firstName }}</p>
<p>lastName: {{ lastName }}</p>
<input type="text" v-model="fullName" />
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'foo',
lastName: 'bar',
}
},
computed: {
fullName: {
// 给 fullName 赋值会自动触发 set 方法
set(newValue) {
const arr = newValue.split('~')
this.firstName = arr[0]
this.lastName = arr[1]
},
// 使用 fullName 的值触发 get 方法
get() {
return this.firstName + '~' + this.lastName
},
},
},
}
</script>
案例-单选影响全选
单选框都选中的时候,全选自动选中。
分析
-
需求:单选都选中,全选自动选中。
-
步骤。
- 复制静态结构和数据。
- 循环生成复选框和文字,用
obj.checked
属性和单选框进行 v-model 双向绑定。 - 定义 isAll 计算属性,值通过每一个单选框的 checked 属性计算而来。
- 使用 v-model 绑定 isAll,作用于全选 checkbox。
静态结构和数据(直接复制)。
<template>
<div>
<span>全选:</span>
<input type="checkbox" />
<button>反选</button>
<ul>
<li>
<input type="checkbox" />
<span>任务名</span>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
arr: [
{
name: '猪八戒',
checked: false,
},
{
name: '孙悟空',
checked: false,
},
{
name: '唐僧',
checked: false,
},
{
name: '白龙马',
checked: false,
},
],
}
},
}
</script>
代码
<template>
<div>
<span>全选:</span>
<input type="checkbox" v-model="isAll" />
<button>反选</button>
<ul>
<li v-for="(obj, index) in arr" :key="index">
<input type="checkbox" v-model="obj.checked" />
<span>{{ obj.name }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
arr: [
{
name: '猪八戒',
checked: false,
},
{
name: '孙悟空',
checked: false,
},
{
name: '唐僧',
checked: false,
},
{
name: '白龙马',
checked: false,
},
],
}
},
computed: {
isAll() {
return this.arr.every((obj) => obj.checked === true)
},
},
}
</script>
案例 全选影响单选
步骤
- 改变 isAll 计算属性为完整写法,在 set 中获取到全选状态。
- 把全选状态同步给所有单选框。
<script>
export default {
// ...其他代码
computed: {
isAll: {
set(val) {
this.arr.forEach((obj) => (obj.checked = val))
},
get() {
return this.arr.every((obj) => obj.checked === true)
},
},
},
}
</script>
案例-反选
目标
反选功能:点击反选,让所有单选框,各自取反。
<button @click="btn">反选</button>
<script>
export default {
// ...其他代码省略
methods: {
btn() {
this.arr.forEach((obj) => (obj.checked = !obj.checked))
},
},
}
</script>
Vue 侦听器-watch
可以侦听 data/computed 属性值改变,然后做出相应的操作。
学习
语法如下。
{
watch: {
"被侦听的属性名" (newVal, oldVal){
}
}
}
需求:侦听 name 的变化,打印变化后的新值。
<template>
<div>
<input type="text" v-model="name" />
</div>
</template>
<script>
export default {
data() {
return {
name: '',
}
},
watch: {
name(newVal, oldVal) {
console.log(newVal, oldVal)
},
},
}
</script>
等价写法。
<script>
export default {
data() {
return {
name: '',
}
},
watch: {
name: {
// handler 是固定的名字
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
},
},
}
</script>
Vue 侦听对象内部数据的变化
<template>
<div>
<input type="text" v-model="user.name" />
<input type="text" v-model="user.age" />
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '',
age: 0,
},
}
},
watch: {
// 只有 user 本身的变化才能被监听到
/* user(newValue) {
console.log(newValue)
}, */
user: {
handler(newVal) {
console.log(newVal)
},
deep: true, // 深度监听 user 内部数据的变化
},
},
}
</script>
案例-品牌管理(数据缓存)
持久化存储品牌数据。
1. 在 watch 侦听 list 变化的时候,把最新的数组 list 转成 JSON 字符串存入到本地。
2. data 里默认把 list 变量从本地取值,如果取不到给个默认的空数组。
效果
<script>
import moment from 'moment'
export default {
data() {
return {
name: '',
price: 0,
// #2 本地取 list
list: JSON.parse(localStorage.getItem('pList')) || [],
}
},
// ...其他代码省略
watch: {
list: {
handler() {
// #1 存入本地
localStorage.setItem('pList', JSON.stringify(this.list))
},
deep: true,
},
},
}
</script>
转载自:https://juejin.cn/post/7158523239432945678