条件渲染
v-if,v-else-if, v-else
v-if、v-else、v-else-if用于根据条件来渲染某一块的内容:
- 这些内容只有在条件为true时,才会被渲染出来
- 这三个指令与JavaScript的条件语句if、else、else if类似
v-if的渲染原理:
-
v-if是惰性的
-
当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉
-
当条件为true时,才会真正渲染条件块中的内容
<template id="template">
<h2 v-if="score >= 90">优秀</h2>
<h2 v-else-if="score >= 60">及格</h2>
<h2 v-else>不及格</h2>
</template>
template
因为v-if是一个指令,所以必须将其添加到一个元素上
如果我们希望切换的是多个元素, 我们可以按照如下方式进行操作
<div v-if="isShow">
<h2>Hello World</h2>
<h2>Hello Vue</h2>
</div>
<!--
如果此时isShow的值为true,那么渲染后的实际dom结构为
<div>
<h2>Hello World</h2>
<h2>Hello Vue</h2>
</div>
-->
此时很明显,最外层的div是没有存在的必要的, 这个时候,我们可以选择使用template
template元素可以当做不可见的包裹元素,并且在v-if上使用,但是最终template不会被渲染出来
<template v-if="isShow">
<h2>Hello World</h2>
<h2>Hello Vue</h2>
</template>
<!--
如果此时isShow的值为true,那么渲染后的实际dom结构为
<h2>Hello World</h2>
<h2>Hello Vue</h2>
-->
v-show
v-show和v-if在功能上是基本类似的,但是在实际上他们依旧是存在区别的:
-
v-show不支持template, 不可以和v-else,v-else-if等一起使用
-
v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行切换,而v-if当条件为false时,其对应的元素压根不会被渲染到DOM中。
所以:
如果我们的元素需要在显示和隐藏之间频繁的切换,那么使用v-show;
如果不会频繁的发生切换,那么使用v-if;
<h2 v-if="isShow">Hello World</h2>
<h2 v-if="isShow">Hello World</h2>
<!-- 当isShow的值为false的时候,实际渲染结果如下: -->
<!--v-if-->
<h2 style="display: none;">Hello World</h2>
列表渲染
v-for
v-for的基本格式是 "数据项 in 可迭代数据(如对象,数组)"
遍历对象
<!--
括号可以省略,但是并不推荐
单个值的时候
例如: value in userInfo
多个值的时候
value, key, index in userInfo
可以使用of来替换in, 来实现相同的功能
(value, key, index) of userInfo
-->
<template v-for="(value, key, index) in userInfo">
<div>index: {{ index }}</div>
<div>key: {{ key }}</div>
<div>value: {{ value }}</div>
</template>
<script>
Vue.createApp({
template: '#template',
data() {
return {
userInfo: {
name: 'Klaus',
age: 23
}
}
}
}).mount('#app')
</script>
遍历数组
<div v-for="(value, index) in users">
{{ index }} --- {{ value }}
</div>
<script>
Vue.createApp({
template: '#template',
data() {
return {
users: [
'Alex',
'Klaus',
'Steven'
]
}
}
}).mount('#app')
</script>
遍历数字
<div v-for="(value, index) in 3">
{{ index }} --- {{ value }}
</div>
<!--
渲染结果为
0 --- 1
1 --- 2
2 --- 3
-->
同样的,我们可以使用 template 元素来循环渲染一段包含多个元素的内容,以避免在渲染的过程中,出现多余的不必要元素
数组更新检测
Vue 将被侦听的数组的变更方法进行了包裹,而数组的更新方法也分为了2类。
- 会改变原数组,所以当数组发生改变的时候,会触发视图更新。例如
pop,push,shift,unshift,splice,sort,reverse - 不会修改原数组,所以需要使用新数组将旧数组的值覆盖掉,才会触发数组更新,例如
slice,concat,filter
key
在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性
key属性的作用:
-
key属性主要用在Vue的
虚拟DOM Diff算法,用以给vnode一个唯一的标识, 方便vue在更新的时候,更好的识别新旧VNode,从而提升DOM的更新效率 -
在
不使用key进行dom更新的时候,vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法 -
如果在更新的时候使用了key,vue会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素
vnode
VNode的全称是Virtual Node,也就是虚拟节点
为了提升更新效率,不频繁操作dom,无论是组件还是元素,vue在解析的时候,会先将template中的内容解析为一个个的VNode
这一个个VNode组合在一个形成了虚拟DOM树(Virtual DOM ===> VDom), 随后vue才会将VDom渲染成真实DOM
VDOM中的每一个VNode 和 真实DOM中的Node是一一对应,一一映射的
VNode的本质是一个用于描述html标签或组件的JavaScript的对象
<!-- 模板中的内容 -->
<div class="title" style="color: red; font-size: 20px">Hello World</div>
经过vue的解析后,会形成类似下边格式的对象
const VNode = {
type: 'div',
props: {
class: 'title',
style: {
color: 'red',
'font-size': '20px'
}
},
// 如果一个节点下有多个子节点,那么children的类型应该为一个数组
children: 'Hello World'
}
vue的模板浏览器是无法直接解析的,所以需要先有
vue-template-compiler将其解析为VDOM随后才可以将VDOM交给浏览器去渲染成真实DOM
插入F案例
需求
原数组 ==> [ 'A', 'B', 'C', 'D' ]
在C的前面插入F
新数组 ==> [ 'A', 'B', 'F', C', 'D' ]
在数组中,以数组的值,即A,B,C,D,F,作为每一个节点的key
实现
Vue事实上会对于有key和没有key会调用两个不同的方法:
- 有key,那么就使用 patchKeyedChildren方法
- 没有key,那么久使用 patchUnkeyedChildren方法
unkeyed
- c和d来说它们事实上并不需要有任何的改动
- 但是因为我们的c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增
- 所以如果没有key的时候,默认的dom diff算法的效率并不是很高
const patchUnkeyedChildren = (oldChildren = [], newChildren = []) => {
const oldLength = oldChildren.length
const newLength = newChildren.length
// 以长度短的那个进行遍历
const commonLength = Math.min(oldLength, newLength)
for (let i = 0; i < commonLength; i++) {
const nextChild = newChildren[i]
// 依次遍历比较更新
patch( oldChildren[i], nextChild)
}
if (oldLength > newLength) {
// remove old
unmountChildren(oldChildren)
} else {
// mount new
mountChildren(newChildren)
}
}
keyed
const patchKeyedChildren = (oldChildren, newChildren) => {
let i = 0
const newChilrenLength= newChildren.length
let endIndexOfOldChildren = oldChildren.length - 1 // prev ending index
let newIndexOfOldChildren = newChilrenLength- 1 // next ending index
// 1. 从头往前对相同类型节点进行更新
// 2. 从后往前对相同类型节点进行更新
// 3. common sequence + mount
// 4. common sequence + unmount
// 5. unknown sequence + vue会尝试移动可以复用的节点并移除和新增对应的节点
}
function isSameVNodeType(oldNode, newNode) {
// 如果一个节点的类型一致,且key是一致的,那么vue就认为该节点没有发生改变,为同一个节点
return oldNode.type === newNode.type && oldNode.key === newNode.key
}
从前往后进行相同节点的更新
- a和b是一致的会继续进行比较
- c和f因为key不一致,所以就会break跳出循环
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= endIndexOfOldChildren && i <= newIndexOfOldChildren) {
const oldNode = oldChildren[i]
const newNode = newChildren[i]
if (isSameVNodeType(oldNode, newNode)) {
patch(oldNode, newNode)
} else {
// 不是同一节点,跳出循环
break
}
i++
}
从后往前进行遍历
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= endIndexOfOldChildren && i <= newIndexOfOldChildren) {
// 分别取出新旧children的最后一个节点
const oldNode = oldChildren[endIndexOfOldChildren]
const newNode = newChildren[newIndexOfOldChildren]
if (isSameVNodeType(oldNode, newNode)) {
patch(oldNode, newNode)
} else {
// 节点不一致跳出循环
break
}
endIndexOfOldChildren--
newIndexOfOldChildren--
}
common sequence + mount
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, endIndexOfOldChildren = 1, newIndexOfOldChildren = 2
// (a b)
// c (a b)
// i = 0, endIndexOfOldChildren = -1, newIndexOfOldChildren = 0
if (i > endIndexOfOldChildren) {
if (i <= newIndexOfOldChildren) {
while (i <= newIndexOfOldChildren) {
// patch函数中如果旧节点不存在就是新增新节点
// 如果旧节点存在,就是更新旧节点
patch(null, newChildren[i])
i++
}
}
}
common sequence + unmount
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, endIndexOfOldChildren = 2, newIndexOfOldChildren = 1
// a (b c)
// (b c)
// i = 0, endIndexOfOldChildren = 0, newIndexOfOldChildren = -1
else if (i > newIndexOfOldChildren) {
while (i <= endIndexOfOldChildren) {
unmount(oldChildren[i])
i++
}
}
unknown sequence
vue会尽可能的尝试移动可以复用的节点,并移除不使用的旧节点,并新增对应的节点
在经历了前4步的比较后,发现unknown sequence内容如下:
-
根据新节点的个数,创建同等长度数组 ---> [0, 0, 0, 0]
-
根据新节点的顺序,遍历旧的节点
- 如果旧节点存在,那么就将新节点在旧节点的索引+1,存储到数组对应位置 (新增记为0,为了避免冲突,所以对应索引值+1)
- 如果旧节点不存在,那么就是新增,对应数组位置记为0
- 如果旧节点没有在新节点数组中存在映射,那么移除该旧节点
- 在遍历旧节点列表的时候,如果在旧节点列表中存在,那么在记录索引的同时,会对新旧节点进行
patch操作
所以此时数组变为了[3, 4, 0, 1]
- 根据
最长递增子序列, 我们可以知道3,4为最长递增子序列,所以D, E是不需要移动的 I是新节点,执行mount操作C是需要移动的节点,从索引为0的位置移动到索引为3的位置
在循环中,为每一个循环项,添加上独一无二的,不会改变的key的时候
vue在进行
DOM diff的时候,可以极大程度的提升我们的更新性能