Vue3 基础 – 快速上手 & 常用指令 & :key 的原理
版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!
1. 在 HTML 网页中使用 vue3 的3个基本步骤
-
通过
script
标签的src
属性,在当前网页中全局引入 vue3 的脚本文件:<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
-
创建 vue3 的单页面应用程序实例:
// 2.1 从 Vue 对象中解构出 createApp 函数 const { createApp } = Vue // 2.2 调用 createApp 这个函数,就能够创建出一个单页面应用程序的实例 const app = createApp() // 2.3 调用 app 实例对象上的 mount() 函数, // 指定单页面应用程序 app,实际要控制页面上哪个区域的渲染 app.mount('#app')
-
声明 vue3 的单页面应用程序实例,实际要控制的页面区域:
<!-- 注意:如果内容为空,则 vue3 会在提示一个警告消息: [Vue warn]: Component is missing template or render function. at <App> --> <div id="app"></div>
2. 定义和渲染数据
-
在调用
createApp()
函数时,可以提供一个对象作为配置参数
,例如:const app = createApp({ /*配置对象*/ })
-
如果想提供要渲染的数据,可以在步骤1的配置对象中,通过
data
节点提供渲染期间要使用的数据:const app = createApp({ // 2.1 注意:data 节点是一个函数 data() { // 2.2 在 data 函数内部,return 的这个对象,就是数据对象, // 要渲染的数据,可以直接写到这个对象中,例如 return { name: 'zs' } return {} } })
-
在步骤2的 data 节点中,定义一个名为
name
的数据,值是liulongbin
:const app = createApp({ data() { return { name: 'liulongbin' } } })
-
在 vue3 控制的模板结构中,使用
{{ 数据名 }}
语法,把数据渲染出来:<div id="app"> <h1>大家好,我是:{{ name }}</h1> </div>
-
拓展:当我们修改 data 节点下的数据后,即可看到页面上的 HTML 内容会自动被刷新。这就是 vue 的强大之处:数据驱动视图。修改 data 数据的示例代码如下:
app._instance.proxy.name = 'escook'
3. vue3 中常用的渲染指令
在 vue 中,指令是带有 v-
前缀的特殊 attribute,它是 vue 提供的特殊语法,大家有必要掌握 vue 中常用指令的使用。
指令能够辅助前端程序员高效地把数据渲染为 HTML 的结构,而程序员不需要调用任何操作 DOM 的 API。
3.0 常用指令的分类
- 内容渲染指令
- 属性绑定指令
- 双向绑定指令
- 条件渲染指令
- 事件绑定指令
- 列表渲染指令
3.1 内容渲染指令
1. 插值表达式
插值表达式(又叫做:Mustache)的语法为 {{ }}
,vue 在解析模板期间,会把 {{ }}
所在的位置,替换为对应的数据值,例如:
<h1>大家好,我是:{{ name }}</h1>
vue 会把 name
的值,替换到 {{ name }}
所在的位置。
注意:插值表达式 {{ }} 是唯一一个不以 v- 前缀开头的指令。
2. v-text
v-text
指令用来填充 HTML 元素的内容,如果 HTML 元素内部有其它内容存在,则会被覆盖掉。语法格式如下:
<h3 v-text="msg">是兄弟就来</h3>
对应的数据为:
const app = createApp({
data() {
return {
msg: '砍我吧'
}
}
})
注意:由于 v-text 指令存在覆盖已有内容的问题,所以在实际开发中它很少被用到。最常用的还是 {{ }} 插值表达式,因为它只是占位符,不会覆盖已有内容。
3. v-html
v-html
指令用来渲染带有 HTML 标记的文本内容,它可以把 HTML 标记解析为真正的 HTML 元素,并插入到模板中渲染。
而插值表达式和 v-text 指令只会把 HTML 标记渲染为纯文本,而不是 HTML。
v-html 的语法格式如下:
<div v-html="rawHtml"></div>
对应的数据为:
const app = createApp({
data() {
return {
rawHtml: '<span style="color: red;">少年强则国强</span>'
}
}
})
3.2 属性绑定指令
1. v-bind
v-bind
指令用来为元素的属性绑定动态的属性值。指令语法如下:
<div v-bind:title="titleMsg">xxx</div>
对应的数据为:
const app = createApp({
data() {
return {
titleMsg: '哇哈哈'
}
}
})
又例如,为图片的 src 属性动态绑定属性的值:
<img v-bind:src="url" />
对应的数据为:
const app = createApp({
data() {
return {
url: 'https://img.yzcdn.cn/vant/cat.jpeg'
}
}
})
2. v-bind 的简写
在实际开发中,v-bind
指令的使用频率非常高,为了简化它的写法,vue 规定 v-bind
指令可以简写为英文的 :
且二者是完全等价的。如上面的例子可以使用 :
简写为:
<div :title="titleMsg">xxx</div>
<img :src="url" />
对应的数据为:
const app = createApp({
data() {
return {
titleMsg: '哇哈哈',
url: 'https://img.yzcdn.cn/vant/cat.jpeg'
}
}
})
注意:今后在 vue 项目开发中,只要看到某个属性前面出现了英文的 : 那么,一定是为这个属性绑定了动态的值。
3. 绑定布尔值
在 vue 中,某些属性的取值可以是布尔值 true 或 false,表示当前的属性是否应该应用于当前的元素。例如 disabled
属性:
<!-- 禁用按钮A -->
<button :disabled="true">按钮A</button>
<!-- 不禁用按钮B -->
<button :disabled="false">按钮B</button>
与之类似的,还有 radio 和 checkbox 的 checked
属性:
<!-- 默认选中“男” -->
<input type="radio" name="gender" :checked="true">男
<input type="radio" name="gender">女
<!-- 默认选中“足球”和“乒乓球” -->
<input type="checkbox" name="hobby">篮球
<input type="checkbox" name="hobby" :checked="true">足球
<input type="checkbox" name="hobby" :checked="true">乒乓球
另外,表单元素 select
下的 option
选项的 selected
属性,也可以绑定布尔值:
<select>
<option value="北京">北京</option>
<option value="上海" :selected="true">上海</option>
<option value="广州">广州</option>
</select>
4. 动态绑定多个值
如果要为某个元素同时绑定多个动态的属性值,可以把多个动态属性封装为一个 JavaScript 对象:
const app = createApp({
data() {
return {
// propObj 对象中封装了一系列属性的键值对
attrsObj: {
id: 'box',
class: 'container',
title: '布局容器'
}
}
}
})
通过不带参数的 v-bind
指令,即可方便的把 attrsObj 对象中封装的属性,一次性绑定到对应的元素上:
<div v-bind="attrsObj">顶部 header 区域</div>
注意:不带参数的 v-bind 指令,指的是省略了
:属性名
的用法。
5. 拓展:使用 JavaScript 表达式
-
在 vue 的数据绑定中,除了支持简单的属性名绑定之外,还支持完整的 JavaScript 表达式绑定。
-
例如,以下这些都属于简单的属性名绑定,它们是直接把 data 中数据项的名字,绑定到了模板中:
<div>我是:{{ name }}</div> <div v-text="msg"></div> <img :src="url" />
-
除此之外,还支持表达式的绑定,例如:
<!-- 函数的调用 & 数学运算 --> <div>我是:{{ name.toUpperCase() }},我今年{{ age + 1 }}岁了。</div> <!-- 函数的调用 --> <div v-text="msg.split('').reverse().join('')"></div> <!-- 字符串的拼接 --> <img :src="'https://img.yzcdn.cn/vant/' + url" /> <!-- 三元表达式 --> <div>{{ age >= 18 ? '抽烟喝酒烫头' : '可乐牛奶娃哈哈' }}</div>
对应的数据如下:
const app = createApp({ data() { return { name: 'liulongbin', age: 17, msg: '冯绍峰', url: 'cat.jpeg' } } })
3.3 双向绑定指令
v-model
双向绑定指令,简化了表单元素的赋值和取值操作。
v-model
的作用:
- data 数据源发生变化,自动重新渲染页面
- 表单数据发生变化,自动更新到 data 数据源中
1. 文本框的双向绑定
input 元素通过 v-model 指令,可以方便地进行赋值和取值,示例代码如下:
<p>Message 的值是:{{ message }}</p>
<input type="text" v-model="message">
对应的数据如下:
const app = createApp({
data() {
return {
message: 'hello'
}
}
})
2. 多行文本框的双向绑定
textarea 元素通过 v-model 指令,可以方便地进行赋值和取值,示例代码如下:
<p>Message 的值是:</p>
<pre>{{ message }}</pre>
<textarea v-model="message"></textarea>
对应的数据如下:
const app = createApp({
data() {
return {
// 注意:这里的 \n 是换行符
message: 'hello \nworld.'
}
}
})
3. 复选框的双向绑定
-
单一复选框的双向绑定,绑定的是布尔类型的值:
<p>复选框选中的flag值为:{{flag}}</p> <input type="checkbox" v-model="flag">
对应的数据如下:
const app = createApp({ data() { return { // 是否被选中 flag: false } } })
-
多个复选框的双向绑定,绑定的是数组类型的值,而且每个 checkbox 必须通过 value 属性提供选中项的值:
<p>多个复选框选中的 hobbies 值为:{{ hobbies }}</p> <label><input type="checkbox" v-model="hobbies" value="篮球">篮球</label> <label><input type="checkbox" v-model="hobbies" value="足球">足球</label> <label><input type="checkbox" v-model="hobbies" value="冰球">冰球</label>
对应的数据如下:
const app = createApp({ data() { return { // 选中的值 hobbies: [] } } })
4. 单选按钮的双向绑定
单选按钮的特点是多选一,所以对单选按钮进行双向绑定时,需要把多个单选按钮通过 v-model 指令绑定到同一个数据源,并通过 value 属性指定选中后的值:
<p>单选按钮选中的 gender 值为:{{ gender }}</p>
<label><input type="radio" v-model="gender" value="男">男</label>
<label><input type="radio" v-model="gender" value="女">女</label>
对应的数据如下:
const app = createApp({
data() {
return {
// 选中的值
gender: '男'
}
}
})
5. 选择器的双向绑定
-
单选选择器的双向绑定,只允许选中一个值:
<p>选中的城市为:{{ city }}</p> <select v-model="city"> <option value="">请选择</option> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="nanjing">南京</option> </select>
对应的数据如下:
const app = createApp({ data() { return { city: '' } } })
-
多选选择器的双向绑定,允许选中多个值,所以需要绑定数组格式的数据源:
<p>选中的城市为:{{ areas }}</p> <select v-model="areas" multiple> <option value="shunyi">顺义区</option> <option value="haidian">海淀区</option> <option value="daxing">大兴区</option> </select>
对应的数据如下:
const app = createApp({ data() { return { areas: [] } } })
6. v-model 的 .lazy 修饰符
默认情况下,v-model
会在每次 input
事件后更新数据。可以添加 .lazy
修饰符来改为在每次 change
事件后更新数据:
<input v-model.lazy="msg" />
7. v-model 的 .number 修饰符
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
<input v-model.number="age" />
注意:
- 如果该值无法被
parseFloat()
处理,那么将返回原始值。 number
修饰符会在输入框有type="number"
时自动启用。
8. v-model 的 .trim 修饰符
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
<input v-model.trim="msg" />
3.4 条件渲染指令
条件渲染指令用来条件性地渲染页面上的某一部分内容。只有表达式的条件成立,才会真正渲染这一部分的内容。
常用的条件渲染指令是 v-if
、v-else
和 v-else-if
。其中,v-if
指令可以单独使用,也可以结合 v-else
和 v-else-if
指令实现两个或多个条件的按需渲染。
1. v-if 的使用
v-if 的语法格式如下:
<div v-if="表达式"></div>
其中,只有表达式的返回值为 true 时,才会真正渲染被 v-if 指令控制的 div 元素。
如果 v-if 的表达式返回值为 false,则被 v-if 指令控制的 div 不会被渲染到浏览器中。
例如:
<div v-if="flag">无敌是多么的寂寞</div>
对应的数据为:
const app = createApp({
data() {
return {
flag: true
}
}
})
2. v-if 结合 v-else 的使用
v-if
指令可以结合 v-else
指令一起使用。
当条件为真时渲染被 v-if
指令控制的元素,当条件为假时渲染被 v-else
指令控制的元素。例如:
<div v-if="age >= 18">抽烟喝酒烫头</div>
<div v-else>牛奶可乐娃哈哈</div>
注意:v-else 指令不需要通过 = 指定相应的表达式,因为 v-else 是兜底的条件,只要前面的所有条件都不满足,那么必然会触发 v-else 的执行。
3. v-if 结合 v-else-if 和 v-else 的使用
v-if
指令可以结合 v-else-if
和 v-else
指令一起使用,从而组成复杂的条件渲染逻辑。
当 v-if
或某个 v-else-if
相应的条件为真时,被控制的元素才会被渲染。
最后的 v-else
依然是兜底的条件,当所有的 v-if
和 v-else-if
条件都不成立时,才会触发 v-else 的执行。例如:
<div v-if="score === 'A'">优秀</div>
<div v-else-if="score === 'B'">良好</div>
<div v-else-if="score === 'C'">一般</div>
<div v-else>差</div>
对应的数据为:
const app = createApp({
data() {
return {
score: 'A'
}
}
})
4. <template> 上的 v-if
正常情况下 v-if
指令只能控制单个元素的显示和隐藏。如果需要使用 v-if
控制一组元素的显示和隐藏,就需要在这一组元素之外包裹一个 div 作为容器,并将 v-if
指令应用于 div 容器之上,例如:
<div v-if="true">
<h1>咏鹅</h1>
<p>鹅鹅鹅,曲项向天歌。</p>
<p>白毛浮绿水,红掌拨清波。</p>
</div>
这么做虽然能实现需求,但会在页面上渲染出一个多余的 div 容器。
更好的方案是使用 vue 内置的 <template>
元素作为外层包裹性质的容器,因为它不会被渲染为实际的元素,只起到包裹性质的作用。例如:
<template v-if="true">
<h1>咏鹅</h1>
<p>鹅鹅鹅,曲项向天歌。</p>
<p>白毛浮绿水,红掌拨清波。</p>
</template>
5. v-show 指令的使用
另一个可以用来实现条件渲染的指令是 v-show
。它的语法格式如下:
<h1 v-show="flag">Hello!</h1>
如果表达式的值为 true,则被控制的元素会被显示;
如果表达式的值为 false,则被控制的元素会被隐藏。
注意:v-show 指令不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。
6. v-if 和 v-show 的对比
相同点:
v-if
和 v-show
指令都能控制元素的条件渲染。
- 如果表达式的值为 true,则被控制的元素会被显示;
- 如果表达式的值为 false,则被控制的元素会被隐藏。
不同点:
- 控制元素显示和隐藏的手段不同:
v-if
指令会动态创建和删除被控制的元素,从而达到切换元素显示和隐藏的目的;v-show
指令仅切换了被控制元素上名为display
的 CSS 属性,从而达到切换元素显示和隐藏的目的;
- 初始渲染的性能不同:
- 如果初始渲染时,表达式的值为 false,则 v-if 的性能更好。
- 如果初始渲染时,表达式的值为 true,则二者性能相近。
- 频繁切换时的性能不同:
- 如果需要频繁切换元素的显示和隐藏,则 v-show 的性能更好。
- 如果不需要频繁切换元素的显示和隐藏,则可以忽略二者的性能差别。
- 总结:
v-if
有更高的切换开销v-show
有更高的初始渲染开销
3.5 事件绑定指令
1. 事件绑定的基本语法
为了响应用户对 DOM 元素的操作,vue 提供了事件绑定指令 v-on
(简写为 @
)。
当监听到 DOM 事件的触发时,会执行对应的 JavaScript 逻辑。它的语法格式为 v-on:事件名="handler"
或 @事件名="handler"
。例如:
<!-- v-on 是事件绑定指令 -->
<button v-on:click="show">按钮</button>
<!-- @ 是 v-on 指令的简写形式 -->
<button @click="show">按钮</button>
上述代码演示了如何为 button 按钮绑定 click
点击事件。
除此之外,vue 还支持绑定其它类型的事件,这里就不再一一例举了。因为把 DOM 原生事件前面的 on
替换成 v-on:
或 @
就变成了 vue 的事件绑定形式,例如:
- onclick –> @click
- oninput –> @input
- onchange –> @change
2. 方法事件处理器
方法事件处理器指的是:指定一个方法作为事件的处理器。例如下面的代码所示,指定了一个 show
方法作为 click
事件的处理器:
<button @click="show">按钮</button>
show
方法作为事件处理器,需要定义在 methods
节点下,例如:
const app = createApp({
data() {
return {}
},
methods: {
show(event) {
console.log('ok')
console.log(event.target.tagName)
}
}
})
在方法事件处理器的参数列表中,第一个形参 event
是事件对象。
3. 基于方法事件处理器实现数值自增
-
声明模板结构如下:
<p>count的值为:{{ count }}</p> <button @click="add">+1</button>
-
在 data 中声明数据源
count
,在 methods 中声明事件处理器add
,代码如下:const app = createApp({ data() { return { count: 0 } }, methods: { add() { app._instance.proxy.count++ } } })
注意:methods 节点下的方法中,this 指向的就是 app._instance.proxy。所以上述代码完全可以替换为 this.count++
4. 内联事件处理器
内联事件处理器相当于原生 DOM 中的内联 JavaScript,例如数值自增的操作,可以简写成内联事件处理器的形式:
<p>count的值为:{{ count }}</p>
<button @click="count++">+1</button>
对应的数据为:
const app = createApp({
data() {
return {
count: 0
}
}
})
注意:内联事件处理器通常用于简单的业务场景,如果涉及到复杂的业务逻辑,请使用方法事件处理器或在内联处理器中调用方法。
5. 在内联处理器中调用方法
首先,我们要能够明确的区分开方法事件处理器和内联事件处理器。
如果事件绑定的处理器是个纯粹的方法名,则是方法事件处理器,例如:
<button @click="show">按钮A</button>
除此之外,其它绑定事件处理器的形式,都是内联事件处理器,例如:
<!-- 绑定内联的 JavaScript -->
<button @click="count++">按钮C</button>
<!-- 绑定了一个方法的调用 -->
<button @click="show()">按钮B</button>
<!-- 绑定了方法的调用的同时,传递参数 -->
<button @click="show('Hello world.')">按钮B</button>
内联事件处理器的优点:解锁了模板向处理器方法传递参数的能力。
6. 在内联事件处理器中访问事件对象
内联事件处理器的缺点:事件对象丢失了,无法在处理器方法中访问到事件对象 event。
上述问题的解决方案有两个,分别是:
-
使用特殊的 $event 变量
-
使用内联箭头函数接收并传递 event 对象
解决方案1:使用特殊的 $event 变量
<button @click="showMsg('hello world.', $event)">按钮</button>
对应的 showMsg
处理器为:
const app = createApp({
methods: {
showMsg(msg, event) {
// 改变按钮显示的文本
event.target.innerHTML = msg
// 改变按钮的背景颜色
event.target.style.backgroundColor = 'cyan'
}
}
})
解决方案2:使用内联箭头函数接收并传递 event 对象
<button @click="(event) => showMsg('你好,世界。', event)">按钮</button>
对应的 showMsg
处理器为:
const app = createApp({
methods: {
showMsg(msg, event) {
// 改变按钮显示的文本
event.target.innerHTML = msg
// 改变按钮的背景颜色
event.target.style.backgroundColor = 'cyan'
}
}
})
7. 事件修饰符
在原生 DOM 的事件处理函数中,如果想要阻止冒泡行为,则需要调用 event.stopPropagation()
;如果想要阻止默认行为,则需要调用 event.preventDefault()
。为了提高用户的开发体验,vue 提供了更优雅的方式来阻止事件冒泡或默认行为,即:事件修饰符。
在 vue 中最常用的两个事件修饰符分别是:
- .prevent
- .stop
其中 .prevnet
用来阻止默认行为,例如:
<!-- 使用 .prevent 修饰了 a 链接的 click 事件 -->
<!-- 点击超链接后,会阻止超链接的默认跳转行为 -->
<a href="https://www.escook.cn/" @click.prevent="showMsg">超链接</a>
另外 .stop
用来阻止事件冒泡,例如:
<div @click="outerHandler">
<!-- 点击内部的 button 按钮,click 事件不会向外冒泡 -->
<!-- 所以外层的 outerHandler 处理器不会执行 -->
<button @click.stop="innerHandler">按钮</button>
</div>
拓展:其它事件修饰符还有 .self、.capture、.once、.passive。具体用法请参考 vue3 官方文档 – 事件修饰符。
8. 按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键,从而执行特定的操作。例如:
- 用户在输入框中按下了 enter 键,则触发提交的函数
- 用户在输入框中按下了 esc 键,则清空文本框的内容
示例代码如下:
<input type="text" v-model="msg" @keyup.enter="submit" @keyup.esc="clear">
对应的 JS 处理逻辑为:
const app = createApp({
data() {
return {
msg: '' // 文本框的数据
}
},
methods: {
// 该处理函数仅在用户按下 enter 键时触发
submit() {
console.log('提交的数据为:' + this.msg)
},
// 该处理函数仅在用户按下 esc 键时触发
clear() {
this.msg = ''
}
}
})
9. 按键别名与按键名的获取
vue 为常用的按键提供了官方内置的按键别名,列表如下:
- .enter
- .tab
- .delete (捕获“Delete”和“Backspace”两个按键)
- .esc
- .space
- .up
- .down
- .left
- .right
如果上述列表中没有你想监听的按键,则可以使用 $event.key
先获取按键的名称,再把获取到的按键名称转为 kebab-case 形式,最后利用转换得到的按键名进行监听即可,例如下面的代码监听了 CapsLock 按键:
<p>输入状态:{{ isUpperCase ? '大写' : '小写' }}</p>
<input type="text" v-model="msg" @keyup.caps-lock="changeMode">
对应的 JS 逻辑为:
const app = createApp({
data() {
return {
msg: '', // 文本框的数据
isUpperCase: false // 是否为大写输入模式
}
},
methods: {
// 仅当用户按下的是 CapsLock 键,才触发此函数的执行
changeMode() {
this.isUpperCase = !this.isUpperCase
}
}
})
10. 系统按键修饰符
如果在触发事件的时候,想要判断用户是否同时按下了 Ctrl、Alt 等系统按键。此时可以使用 vue 内置的系统按键修饰符,主要有以下4个:
- .ctrl
- .alt
- .shift
- .meta [注意:meta 在 Windows 键盘上指的是 Windows 键(⊞),在 Mac 键盘上指的是 Command 键(⌘)]
例如,下面的代码监听了触发 div 的 click 事件时,是否同时按下了特定的系统按键,从而改变 div 的形状和外观:
<!-- 点击 div 的时候, -->
<!-- 1. 如果同时按下了 Ctrl 键,则添加 square 类样式 -->
<!-- 2. 如果同时按下了 Alt 键,则添加 round 类样式 -->
<!-- 3. 如果同时按下了 Shift 键,则还原为默认的 box 类样式 -->
<div class="box" :class="shape"
@click.ctrl="changeShape('square')"
@click.alt="changeShape('round')"
@click.shift="changeShape('')">
</div>
对应的 JS 逻辑为:
const app = createApp({
data() {
return {
// 类样式的名称
shape: ''
}
},
methods: {
// 事件的处理函数
changeShape(shape) {
this.shape = shape
}
}
})
配套的 CSS 样式为:
<style>
.box {
width: 300px;
height: 300px;
background-color: #efefef;
transition: all 1s ease;
}
.square {
border-radius: 20px;
background-color: cyan;
transition: all 1s ease;
}
.round {
border-radius: 50%;
background-color: lightgreen;
transition: all 1s ease;
}
</style>
11. .exact
修饰符
上述的例子中,存在一个很明显的 Bug:
- 我们希望在触发 div 的 click 事件时,仅当用户按下了
Ctrl
或Alt
或Shift
按键时,才触发changeShape
函数 - 现在的 Bug 是,用户在触发 click 事件时,如果按下了
Ctrl + Alt
的组合按键,也会触发changeShape
函数
而 .exact
修饰符可以完美的解决这个问题。.exact
修饰符表示精确匹配系统按键。
因此,我们可以针对上述的例子进行修改,在特定的系统按键修饰符的后面应用 .exact
修饰符,表示精确匹配系统按键:
<div class="box" :class="shape"
@click.ctrl.exact="changeShape('square')"
@click.alt.exact="changeShape('round')"
@click.shift.exact="changeShape('')">
</div>
注意:所谓的精确匹配系统按键,仅对系统按键修饰符生效,如果用户按下了 Ctrl + A 的组合键,也会触发
@click.ctrl.exact
所绑定的事件处理器。
12. 鼠标按键修饰符
vue 还提供了鼠标按键修饰符,用来监听事件是否由特定的鼠标按键触发:
- .left
- .right
- .middle
例如,下面的代码演示了如何阻止在 h1
元素上显示鼠标的右键菜单:
<h1 @click.right.prevent>这是一个标题</h1>
注意:绑定事件时,不一定非要提供事件的处理器,我们也可只提供事件修饰符,从而达到特定的目的。
3.6 列表渲染指令
v-for
指令是 vue 提供的列表渲染指令。
如果您有一个数组,想把数组中的每一项渲染为格式相似的 HTML 结构,那么 v-for 指令可以帮助您实现列表数据的渲染。
使用场景:商品列表、用户列表等。
1. v-for 的基本使用
v-for 的基本语法格式为:
v-for="当前循环项 in 数组"
其中关键字 in
前面的是当前循环项,关键字 in
后面的要循环的数组。例如:
<ul>
<li v-for="item in list">姓名:{{ item.name }},年龄:{{ item.age }}</li>
</ul>
对应的数据为:
const app = createApp({
data() {
return {
// 数组
list: [
{ name: 'zs', age: 20 },
{ name: 'liulongbin', age: 21 },
{ name: 'escook', age: 22 }
]
}
}
})
2. v-for 中的索引
v-for 的完整语法格式为:
v-for="(循环项, 循环项的索引) in 数组"
其中 in
关键字左侧的 ( )
里面,分别是当前循环项和当前循环项的索引。例如:
<ul>
<li v-for="(item, index) in goods">{{ index + 1 }}. {{ item }}</li>
</ul>
对应的数据为:
const app = createApp({
data() {
return {
// 数组
goods: ['手表', '手机', '手串']
}
}
})
注意:v-for 中的索从 0 开始递增。
3. v-for 中的解构
如果 v-for
指令中的循环项 item 是一个对象,则可以在 v-for 指令中进行解构操作,语法格式为:
v-for="{数据A, 数据B} in 数组"
或
v-for="({数据A, 数据B}, 索引) in 数组"
其中 in
关键字左侧的 { }
表示解构操作。例如:
<ul>
<li v-for="{name, age} in list">姓名:{{ name }},年龄:{{ age }}</li>
</ul>
对应的数据为:
const app = createApp({
data() {
return {
// 数组
list: [
{ name: 'zs', age: 20 },
{ name: 'liulongbin', age: 21 },
{ name: 'escook', age: 22 }
]
}
}
})
4. template 上的 v-for
v-for
指令每次只能循环生成一个元素,如果想在每次循环期间生成一组元素,则必须在这一组元素之外包裹一层 div 标签作为容器,并把 v-for
指令作用于外层的 div 容器之上。例如:
<ul>
<div v-for="(item, index) in list">
<li class="divider" v-if="index!== 0"></li>
<li>姓名:{{ item.name }},年龄:{{ item.age }}</li>
</div>
</ul>
对应的 css 样式为:
<style>
.divider {
border-top: 1px solid #888;
list-style: none;
margin: 10px 0;
}
</style>
如上,虽然可以实现列表数据的渲染,但却不尽完美。因为 div
标签在循环中只起到容器的作用,在整个列表结构中没有任何意义。
所以推荐的做法是利用 vue 内置的 <template>
标签替代上述的 div
标签,因为 <template>
是一个虚拟的容器,不会被渲染为实际的元素。
最终,优化过后的代码如下:
<ul>
<template v-for="(item, index) in list">
<li class="divider" v-if="index!== 0"></li>
<li>姓名:{{ item.name }},年龄:{{ item.age }}</li>
</template>
</ul>
思考:分割线的 <li> 能否放在数据的 <li> 之后?如果能,则 v-if 的条件是什么?如果不能,请说出为什么。
5. v-for 与 v-if 的优先级
注意:vue 官方不推荐在一个元素上,同时使用 v-if 和 v-for 指令。因为这样使用无法明确体现出二者的优先级。降低代码的阅读性和维护性。
当 v-if
和 v-for
同时存在于一个元素上的时候,v-if
比 v-for
的优先级更高。这意味着 v-if
的条件将无法访问到 v-for
作用域内定义的变量别名:
<ul>
<!-- 注意:这里的 v-if 指令中,无法访问到 item.done 对应的数据, -->
<!-- 因为 v-if 比 v-for 的优先级高, -->
<!-- 当 v-if 执行的时候访问不到 item 对象,因为 v-for 此时还未执行! -->
<li v-for="item in todos" v-if="!item.done">{{item.task}}</li>
</ul>
对应的数据为:
const app = createApp({
data() {
return {
// 任务列表,done 为 true 表示完成;done 为 false 表示未完成
todos: [
{ task: '晨练', done: true },
{ task: '吃早餐', done: true },
{ task: '吃午饭', done: false },
{ task: '午休', done: false }
]
}
}
})
解决的方案很简单,先循环再判断即可。在 li
元素的外层包裹一个 template
组件,并把 v-for
指令从 li
上挪到 template
组件上,示例代码如下:
<ul>
<template v-for="item in todos">
<li v-if="!item.done">{{item.task}}</li>
</template>
</ul>
改造后的代码除了解决了 v-if
优先级高导致的报错问题之外,还有这3个明显的特征:
<template>
是一个虚拟容器,不会被渲染为任何实际元素,因此不会导致 DOM 结构的冗余- 代码的可读性更强,外层用来循环数组从而得到每个列表项,内部根据列表项的状态实现 DOM 结构的按需渲染
- 如果
item.done
值为false
,则不会渲染对应的 DOM 结构,因此初始的渲染性能较好
3.7 v-for 中的 key 的基本使用
key 值是 v-for 指令中很重要的一个内容,在接下来的课程中,我们先来学习 key 值的基本使用,然后再深入探讨一下 key 值的内部原理。
1. 案例:在 v-for 列表的头部追加元素
在 data 节点下,定义任务列表的 todos
数据:
data() {
return {
// 任务列表
todos: [
{ task: '晨练' },
{ task: '吃早餐' },
{ task: '吃午饭' }
]
}
}
并在模板结构中,使用 v-for
指令循环渲染出列表的结构:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<span>{{ item.task }}</span>
</li>
</ul>
为了能够实现在任务列表的头部添加新任务的功能,还需要在 data 节点下定义名为 taskName
的数据项,表示即将添加到头部的新任务的名称,同时,还要在 methods 节点下定义名为 add
的方法,用来当做添加按钮的 click 事件处理器:
const app = createApp({
data() {
return {
// 要添加的任务名称
taskName: '',
// 任务列表
todos: [
{ task: '晨练' },
{ task: '吃早餐' },
{ task: '吃午饭' }
]
}
},
methods: {
// 添加操作
add() {
// 1. 如果要添加的任务名称为空,则提示警告信息
if (this.taskName.length === 0) return console.warn('任务名称不能为空!')
// 2. 向列表头部添加一个新任务
this.todos.unshift({ task: this.taskName })
// 3. 清空文本框的内容
this.taskName = ''
}
}
})
最后,在模板结构中,新增添加区域的 UI 结构如下:
<div id="app">
<!-- 添加区域 -->
<div>
<input type="text" v-model.trim="taskName">
<button @click="add">添加</button>
</div>
<hr>
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<span>{{ item.task }}</span>
</li>
</ul>
</div>
至此,在头部追加新任务的功能就实现了,经过测试,代码没有任何问题,功能也可以正常使用。
2. 新功能:勾选任务的完成状态
我们在上一个案例的基础之上,继续添加一个勾选任务完成状态的新功能。
修改模板结构中的列表区域,在每个 li
元素中,都添加一个 checkbox
复选框,作为当前任务的完成状态,代码如下:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
</li>
</ul>
改造完成后立即测试当前的功能,我们会发现复选框的勾选状态发生了紊乱。
不用担心,这个问题是由 vue 默认的更新策略所导致的。具体原因我们需要在原理层面进行深入讲解。
那么接下来,我们首先来看一下如何解决这个勾选状态紊乱的问题。
3. BugFix:解决勾选状态紊乱的方案
解决方案,就是把每个复选框的勾选状态和数据源进行绑定,这样 vue 就能够正确更新每个复选框的勾选状态了。
首先,修改 data 下的数据节点,为 todos
下的每个任务,都添加一个 done
属性,用来表示当前任务是否完成:
data() {
return {
// 要添加的任务名称
taskName: '',
// 任务列表
todos: [
{ task: '晨练', done: true },
{ task: '吃早餐', done: false },
{ task: '吃午饭', done: false }
]
}
}
其次,修改 methods 下的 add
方法,在每次添加新任务时,为其附加一个 done: false
的初始完成状态:
methods: {
// 添加操作
add() {
// 1. 如果要添加的任务名称为空,则提示警告信息
if (this.taskName.length === 0) return console.warn('任务名称不能为空!')
// 2. 向列表头部添加一个新任务
this.todos.unshift({ task: this.taskName })
// 3. 清空文本框的内容
this.taskName = ''
}
}
最后,修改模板结构,为每个任务中的 checkbox
复选框进行 v-model
的双向数据绑定:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox" v-model="item.done">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
</li>
</ul>
至此,复选框勾选状态紊乱的问题得到了解决。
但是…如果再为每个任务添加一个文本框的备注呢?文本框的状态会紊乱吗?又该如何解决呢?
4. 新功能:为任务添加备注
我们继续为案例添加新功能,即为每个任务添加一个文本框的作为备注信息。
修改模板结构中的列表区域,在每个 li
元素中,都添加一个文本框,作为当前任务的备注描述,代码如下:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox" v-model="item.done">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
<!-- 文本框备注 -->
<input type="text">
</li>
</ul>
改造完成后立即测试当前的功能,我们会发现在文本框中填写的备注也发生了紊乱。
Don’t worry,这个问题依然是由 vue 默认的更新策略所导致的。具体原因稍后详细介绍。
我们还是先来看一下如何解决这个文本框内容紊乱的问题吧。
5. BugFix:解决文本框内容紊乱的方案
本次的解决方案和解决复选框勾选状态紊乱的方案如出一辙,核心思路就是把文本框的内容和数据源进行双向的绑定即可。
首先,修改 data 下的数据节点,为 todos
下的每个任务,都添加一个 remark
属性,用来表示当前任务的备注描述:
data() {
return {
// 要添加的任务名称
taskName: '',
// 任务列表
todos: [
{ task: '晨练', done: true, remark: '早晨6点' },
{ task: '吃早餐', done: false, remark: '' },
{ task: '吃午饭', done: false, remark: '' }
]
}
}
其次,修改 methods 下的 add
方法,在每次添加新任务时,为其附加一个 remark: ''
的初始备注描述:
methods: {
// 添加操作
add() {
// 1. 如果要添加的任务名称为空,则提示警告信息
if (this.taskName.length === 0) return console.warn('任务名称不能为空!')
// 2. 向列表头部添加一个新任务
this.todos.unshift({ task: this.taskName, done: false, remark: '' })
// 3. 清空文本框的内容
this.taskName = ''
}
}
最后,修改模板结构,为每个任务中的文本框进行 v-model
的双向数据绑定:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox" v-model="item.done">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
<!-- 文本框备注 -->
<input type="text" v-model="item.remark">
</li>
</ul>
至此,文本框内容紊乱的问题也得到了解决。到此为止,我们已经连续两次解决了状态紊乱的问题,是时候做一个小小的总结了:
- 如果列表每一项中的表单元素,没有绑定任何数据源,则必然发生数据紊乱的问题
- 这种没有绑定数据源的情况,我们称之为:临时状态
- 如果列表每一项中的表单元素,和数据源进行了绑定,则数据紊乱的问题必然得到解决
- 这种绑定了数据源的情况,我们称之为:响应式状态
总之,临时状态有可能会发生状态紊乱的问题,而响应式状态不会发生状态紊乱的问题。大家先记住这种表象式的总结,后面会有助于大家在原理层面深入理解这个问题。
6. 基于 key 值解决临时状态紊乱的问题
我们在之前解决临时状态紊乱时,每次都需要为表单元素进行数据的绑定,从而把临时状态变更为响应式状态。
有没有其它方案,能让我们在不改变临时状态的前提下,也能解决状态紊乱的问题呢?这就需要用到 v-for 中的 key 值了。
首先,让我们把页面结构重新梳理一遍,把临时状态紊乱的问题完整的还原出来,data 中的数据如下:
data() {
return {
// 要添加的任务名称
taskName: '',
// 任务列表
todos: [
{ task: '晨练' },
{ task: '吃早餐' },
{ task: '吃午饭' }
]
}
}
methods 中的方法如下:
methods: {
// 添加操作
add() {
// 1. 如果要添加的任务名称为空,则提示警告信息
if (this.taskName.length === 0) return console.warn('任务名称不能为空!')
// 2. 向列表头部添加一个新任务
this.todos.unshift({ task: this.taskName })
// 3. 清空文本框的内容
this.taskName = ''
}
}
模板结构如下:
<div id="app">
<!-- 添加区域 -->
<div>
<input type="text" v-model.trim="taskName">
<button @click="add">添加</button>
</div>
<hr>
<!-- 列表区域 -->
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
<!-- 文本框备注 -->
<input type="text">
</li>
</ul>
</div>
问题还原出来之后,我们就要着手使用 key 值来解决临时状态紊乱的问题了,4个步骤如下:
步骤1:为 data 下的 todos
数据,添加唯一的 id 标识:
data() {
return {
// 要添加的任务名称
taskName: '',
// 任务列表
todos: [
{ id: 1, task: '晨练' },
{ id: 2, task: '吃早餐' },
{ id: 3, task: '吃午饭' }
]
}
}
步骤2:为了保证新增的任务项也能有递增的唯一的 id 值,我们需要在 data 下新增一个名为 nextId
的数据,它的值是下一次添加时可用的 id 值:
data() {
return {
// 要添加的任务名称
taskName: '',
// 下一次添加时可用的 id 值
nextId: 4,
// 任务列表
todos: [
{ id: 1, task: '晨练' },
{ id: 2, task: '吃早餐' },
{ id: 3, task: '吃午饭' }
]
}
},
步骤3:修改 methods下的 add
方法,为每次新增的任务附带唯一的 id 标识,添加完成后记得更新 nextId
的值:
methods: {
// 添加操作
add() {
// 1. 如果要添加的任务名称为空,则提示警告信息
if (this.taskName.length === 0) return console.warn('任务名称不能为空!')
// 2. 向列表头部添加一个新任务
this.todos.unshift({ id: this.nextId, task: this.taskName })
// 3. 清空文本框的内容
this.taskName = ''
// 4. 更新 nextId 的值
this.nextId++
}
}
步骤4:修改模板结构中的 v-for
循环,在 v-for
循环的同时,为当前循环的元素动态绑定 key
属性的值,值为当前循环项的 id,即 item.id
。代码如下:
<!-- 列表区域 -->
<ul>
<li v-for="item in todos" :key="item.id">
<!-- 任务的完成状态 -->
<input type="checkbox">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
<!-- 文本框备注 -->
<input type="text">
</li>
</ul>
到此为止,我们成功利用 v-for
配套的 key 值,解决了临时状态紊乱的问题。好处有2个:
- 无需把临时状态转变为响应式状态
- 只需在循环期间绑定一次 key 值即可解决问题,无需像之前那样为每个表单元素依次定义数据源、再依次进行 v-model 的绑定
7. key 值的使用注意事项
- key 值必须具有唯一性,不能重复
- key 值必须是 number 或 string 或 symbol 类型
- 尽量不要使用索引当做 key 值,因为索引不具有唯一性
- 为了避免出现非预期的 DOM 更新问题,推荐给 v-for 循环的元素绑定唯一的 key 值
简单粗暴的记忆方法:只要使用了 v-for 指令,就一定要加 key 值绑定;而且尽量用 id 当做 key 值,没有 id 的情况下找具有唯一性的 number、string 值当做 key。
3.8 v-for 中 key 值的原理
1. 虚拟 DOM 的概念
虚拟 DOM 又叫做 VNode,本质上是用来描述 DOM 结构的 JavaScript 对象。它在 Vue 中用来描述不同类型的节点,例如普通元素节点、组件节点等。
例如,我们提供如下的 DOM 结构:
<h1 id="topicTitle" title="标题">This is an article title.</h1>
将其转化为 VNode 之后,格式如下:
const vnode = {
type: 'h1',
props: {
id: 'topicTitle',
title: '标题'
},
// 子节点是普通文本
children: 'This is an article title.'
}
其中:
- type 属性表示 DOM 的标签类型
- props 属性表示 DOM 上的一些附加信息,例如 id、title 等
- children 属性表示 DOM 的子节点,可以是文本,也可以是一个 VNode 数组
再例如,我们提供如下的 DOM 结构:
<ul>
<li>第1个子元素</li>
<li>第2个子元素</li>
</ul>
将其转化为 VNode 之后,格式如下:
const vnode = {
type: 'ul',
props: {},
// 子节点是 VNode 数组
children: [{
type: 'li',
props: {},
children: '第1个子元素'
}, {
type: 'li',
props: {},
children: '第2个子元素'
}
]
}
2. 虚拟 DOM 的优势
最大的优势:跨平台渲染,不同的平台可以基于自己的实现,轻松的把 VNode 转化为各自平台的代码
- 把 VNode 渲染为小程序平台的组件
- 把 VNode 渲染为 Weex 移动端的组件
虚拟 DOM 配合 Diff 算法,能够尽可能减少 Vue 框架对 DOM 的操作
如上图所示,如果使用原生操作更新页面结构时,会移除所有旧 DOM 节点,并重新创建新 DOM 节点。
而我们可以明显的观察到,相比于旧 DOM,新 DOM 元素只是顺序上发生了变化:新 DOM 把 P3 移动到了 P1 之前。我们只需要更新 DOM 元素的先后顺序即可,根本不用重新创建所有的元素。
虚拟 DOM 配合 Diff 算法,可以实现旧 DOM 元素的复用,从而减少了不必要的 DOM 销毁和创建的过程。
误区:虚拟 DOM 的性能一定比手动操作原生 DOM 好。请注意,这个说法是错误的。
- 从 DOM 复用性的角度出发来对比两者,虚拟 DOM 的性能比手动操作原生 DOM 要好,这是毋庸置疑的。
- 从 DOM 的创建效率方面出发来对比两者,手动操作原生 DOM 的性能更好。
3. 就地更新策略
假设 data 中存在如下的一组 todos
数据,我们希望把它渲染为页面上的 ul 无序列表:
data() {
return {
// 任务列表
todos: [
{ task: '晨练' },
{ task: '吃早餐' },
{ task: '吃午饭' }
]
}
}
执行的 v-for 指令如下:
<ul>
<li v-for="item in todos">{{ item.task }}</li>
</ul>
生成的虚拟 DOM (VNode)如下所示:
const vnode = {
type: 'ul',
props: {},
// VNode 数组,通过 v-for 指令渲染的子节点
children: [{
type: 'li',
props: { key: null },
children: '晨练'
}, {
type: 'li',
props: { key: null },
children: '吃早餐'
}, {
type: 'li',
props: { key: null },
children: '吃午饭'
}]
}
由于这是第一次渲染页面,所以会直接基于虚拟 DOM 创建真实的 DOM 结构,并把上一步生成的 VNode 标记为旧虚拟 DOM:
<ul>
<li>晨练</li>
<li>吃早餐</li>
<li>吃午饭</li>
</ul>
至此,页面的首次渲染完成。
接下来,如果在 todos
数据的头部插入一个新任务,则对应的数据为:
data() {
return {
// 任务列表
todos: [
{ task: '测试' },
{ task: '晨练' },
{ task: '吃早餐' },
{ task: '吃午饭' }
]
}
}
生成的新虚拟 DOM (VNode)如下所示:
const vnode = {
type: 'ul',
props: {},
// VNode 数组,通过 v-for 指令渲染的子节点
children: [{
type: 'li',
props: { key: null },
children: '测试'
}, {
type: 'li',
props: { key: null },
children: '晨练'
}, {
type: 'li',
props: { key: null },
children: '吃早餐'
}, {
type: 'li',
props: { key: null },
children: '吃午饭'
}]
}
注意:为了提高 DOM 操作的性能,Vue 会尽可能的复用已存在的旧 DOM 元素,从而减少新 DOM 的创建。此时,Vue 会对新旧 VNode 进行对比。
那么对比的策略是什么呢?由于我们没有给 v-for 指令绑定 key 值,所以当前每个 li 对应的 VNode 的 key 默认等于 null。当 key 值为 null 的时候,Vue 的对比策略是:就地更新。
具体来讲,就地更新策略指的是逐层对比新旧 VNode 节点:
- 如果新旧 VNode 类型相同,则进行更新
- 如果新旧 VNode 类型不同,则移除旧元素,创建新元素
- 如果旧 VNode 比新 VNode 多,则移除多余的旧元素
- 如果新 VNode 比旧 VNode 多,则创建多余的新元素
基于就地更新策略,上述的新旧 VNode 对比完成之后,Vue 得到的更新过程为:
- 复用3个 li 的旧 DOM,并更新这三个旧 DOM 的文本子节点
- 创建1个新的 li DOM,并把它添加到 ul 中
4. 就地更新策略和临时状态
掌握了 Vue 的就地更新策略之后,我们再用它分析一下 3.7.2 中的复选框勾选状态紊乱的 Bug。
v-for 的指令为:
<ul>
<li v-for="item in todos">
<!-- 任务的完成状态 -->
<input type="checkbox">
<!-- 任务的名称 -->
<span>{{ item.task }}</span>
</li>
</ul>
对应的数据为:
data() {
return {
// 任务列表
todos: [
{ task: '晨练' },
{ task: '吃早餐' }
]
}
}
首次渲染得到的 VNode 为:
const vnode = {
type: 'ul',
props: {},
// VNode 数组,通过 v-for 指令渲染的子节点
children: [{ // 第1个li(VNode)
type: 'li',
props: { key: null },
children: [{
type: 'input',
props: { type: 'checkbox' }
}, {
type: 'span',
props: {},
children: '晨练'
}]
}, { // 第2个li(VNode)
type: 'li',
props: { key: null },
children: [{
type: 'input',
props: { type: 'checkbox' }
}, {
type: 'span',
props: {},
children: '吃早餐'
}]
}]
}
至此,页面的首次渲染完成。
接下来,如果在 todos
数据的头部插入一个新任务,则对应的数据为:
data() {
return {
// 任务列表
todos: [
{ task: '测试' },
{ task: '晨练' },
{ task: '吃早餐' }
]
}
}
生成的新虚拟 DOM (VNode)如下所示:
const vnode = {
type: 'ul',
props: {},
// VNode 数组,通过 v-for 指令渲染的子节点
children: [{ // 第1个li(VNode)
type: 'li',
props: { key: null },
children: [{
type: 'input',
props: { type: 'checkbox' }
}, {
type: 'span',
props: {},
children: '测试'
}]
}, { // 第2个li(VNode)
type: 'li',
props: { key: null },
children: [{
type: 'input',
props: { type: 'checkbox' }
}, {
type: 'span',
props: {},
children: '晨练'
}]
}, { // 第3个li(VNode)
type: 'li',
props: { key: null },
children: [{
type: 'input',
props: { type: 'checkbox' }
}, {
type: 'span',
props: {},
children: '吃早餐'
}]
}]
}
依据就地更新策略,新旧 VNode 具体的对比过程如下图所示:
经过分析发现,每个 checkbox
复选框的勾选状态,不影响就地更新的对比过程。复选框只是简单粗暴的被就地复用了。因此出现了临时状态紊乱的问题。
5. 就地更新策略和数据绑定
在 3.7.3 中,我们给复选框添加了 v-model
的双向数据绑定,解决了复选框勾选状态紊乱的问题。
这其实很好理解,因为 Vue 在进行新旧 VNode 对比的过程中,会根据数据驱动视图的思想,让真实 DOM 所显示的数据和 data 中的数据保持一致。
但是,临时状态不受数据驱动视图的影响,因为它没有与任何响应式数据进行绑定。
6. key 值更新策略
不同于默认的就地更新策略,key 值更新策略可以最大限度的实现已有元素的重排和重用。
在 key 值更新策略中,Vue 认为:
- 具有相同 key 值的新旧 VNode 是可复用的元素
- 如果新 VNode 对应的 key 值不存在于旧 VNode 中,则当前的新 VNode 需要创建并添加
- 如果旧 VNode 对应的 key 值不存在于旧 VNode 中,则当前的旧 VNode 需要被删除(卸载)
在 3.7.6 中,我们给 v-for
循环生成的 <li>
添加了 :key="item.id"
的 key 值绑定,从而解决了临时状态紊乱的问题。
在列表头部新增一个元素,则 key 值更新策略的执行过程如下:
-
初始 Diff 状态,定义三个变量:
-
第1次Diff时,默认从头部进行 VNode 的对比和复用。发现 j=0 指向的新旧 VNode 的 key 值不同,则放弃从头部 Diff,转而从尾部开始进行 Diff。从尾部进行Diff时,发现新旧 VNode 都是P3,于是复用P3,只对P3进行 patch 更新即可。同时,让
newEnd--
和oldEnd--
,为第2次Diff做好准备: -
第2次Diff时,
newEnd=2
且oldEnd=1
,它们所指向的新旧 VNode 都是P2,key 值一样,可以复用,于是对P2进行 patch 更新。同时让newEnd--
和oldEnd--
,为第3次Diff做好准备: -
第3次Diff时,
newEnd=1
且oldEnd=0
,它们所指向的新旧 VNode 都是P1,key 值一样,可以复用,于是对P1进行 patch 更新。同时让newEnd--
和oldEnd--
,为第4次Diff做好准备: -
第4次Diff时,
newEnd=0
且oldEnd=-1
,同时j=0
。经过分析我们发现:oldEnd < j
,说明旧 VNode 已经处理完毕newEnd >= j
,说明新 VNode 尚未处理完,证明这些尚未处理完的新 Node 是新增的,只需创建并添加到P1之前即可
此次 Diff 过程中,完全复用了 P1 – P3 这三个节点,只新建了 P4 节点。
7. 拓展:key 值更新策略(在中间新增)
-
初始状态下,定义三个变量,分别指向头部和两个尾部节点:
-
先从头部进行 Diff 比较,如果 key 值相同,则进行 patch 更新:
-
第3次Diff时
j=2
,此时新 VNode 是P4,旧 VNode 是P3,key 值不同,则从尾部开始 Diff: -
第4次Diff时,
newEnd=2
且oldEnd=1
,同时j=2
。经过分析我们发现:oldEnd < j
,说明旧 VNode 已经处理完毕newEnd >= j
,说明新 VNode 尚未处理完,证明这些尚未处理完的新 Node 是新增的,只需创建并添加到P3之前即可
此次 Diff 过程中,完全复用了 P1-P3 节点,只新建了一个 P4 节点。
8. 拓展:key 值更新策略(删除元素)
-
初始状态下,定义三个变量,分别指向头部和两个尾部节点:
-
先从头部进行比较,如果 key 值相同,则进行 patch 更新:
-
第3次Diff时
j=2
,此时新 VNode 是P4,旧 VNode 是P3,key 值不同,则从尾部开始 Diff: -
第4次Diff时,
newEnd=1
且oldEnd=2
,同时j=2
。经过分析我们发现:newEnd < j
,说明新 VNode 已经处理完毕oldEnd >= j
,说明旧 VNode 尚未处理完,证明这些尚未处理完的旧 Node 是多余的,只需删除它们即可
此次 Diff 过程中,完全复用了 P1,P2,P4 这三个节点,只删除了一个 P3 节点。
9. 拓展:key 值更新策略(移动元素)
在理想状态下,Diff 算法只需要简单的挂载或卸载节点即可,但在复杂情况下,简单的挂载和卸载节点可能无法处理到所有的节点,例如:
-
初始状态如下:
-
进行先头后尾的 Diff 过程:
-
当头和尾都处理完成后,我们发现还剩下了很多中间的节点没有被处理到:
为了进一步处理中间这些剩余的节点,我们需要了解一下 Diff 算法中最核心、最重要的内容:
-
为新 VNode 中未处理的元素建立索引表:
-
循环旧 VNode,判断是否需要进行移动:
-
创建 source 数组,用来记录新 VNode 在旧 VNode 中对应的真实的索引位置:
-
-
循环旧 VNode,填充 source 数组:
-
根据 source 数组得到最长递增子序列的索引数组:
-
倒序循环每一个新 VNode 节点,根据不同的条件对 DOM 进行创建或移动:
版权归作者 ©刘龙宾 所有,本文章未经作者允许,禁止私自转载!
一条评论
麦苗如墨
简约、严密、丝滑,彬哥牛逼!!!