思考:v-for中的key是什么作用?
在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性
这个key属性有什么作用呢? 在这之前,先了解几个概念:
1.认识虚拟节点(VNode)
VNode是virtual Node的全称,叫做虚拟节点
事实上,无论是组件还是元素,它们最终在vue中表示出来的都是一个个Vnode
VNode的本质是一个Javascript对象:
这里有一个div,在页面上它可能是这样的:
<div class="title",style="font-size:30px;color:red";>哈哈哈</div>
但是本质上,它是这样的:
const vnode = {
type:'div',
props: {
class:'title',
style: {
"font-size":"30px",
color:"red"
},
},
children: "哈哈哈" //如果这个div元素里有多个子节点,那么children为数组
}
2.虚拟DOM
如果这里不是一个简单的div,而是有一大堆元素,那么这些元素会形成一个由很多个VNode形成的树,这个树就叫做虚 拟DOM树,简称虚拟DOM
注意:虚拟节点是单个元素,而虚拟DOM是由多个虚拟节点组成的树
3.插入f的案例
在列表渲染中,我们渲染了四个字母abcd,但是之后我们想在b和c之间插入一个f,在这之前我们先来思考一个问题:
首先当前的数组发生了变化,那么一些元素必定会被重新渲染,那么必定要重新进行遍历。 那重新遍历的话就会有一个问题:
第一次遍历的时候生成了四个VNodes :a b c d,之后在中间插入了一个f,这时候怎么渲染效率是最高的?
第一种方法:
不管三七二十一,插入之后所有的元素都重新生成Vnodes,进行渲染,因为所有的元素都进行了渲染,那么效率必然很低
还有第二种方法:
因为ab不用变,所以就把c的位置改成f,d的位置改成c,最后再添加一个位置给d,这样的话性能也不太高
因为c和d和f都渲染了,但是c和d没必要重新渲染。
第三种办法就是:
你要插入元素,就生成这个元素,之后在要插入的地方直接插入,不要影响其他元素,不要让其他元素去重新渲染,这样的性能是最高的,这就涉及到diff算法。
4.diff算法
那么如何去判断要不要生成元素,然后在某个地方插入呢?
在vue中,元素展示在页面上要经过template模板--->Vnode---->真实DOM三个阶段,
但是在视图发生更新的时候,会生成新的Vnode,试图改变之前的Vnode叫做旧Vnode
将新旧Vnode进行对比的算法,就是diff算法。
事实上有key和没有key,diff算法会采用不同的方式来处理:
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) { //判断有无key
// 有key会调用patchKeyedChildren函数
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
//没有key会调用patchUnkeyedChildren函数
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
5.没有绑定key
没有绑定key时,diff算法其实就会按照我们说的第二种方法来计算,a b不变, c的位置渲染成f, d的位置渲染成c,而最后再添加一个位置给d
也就是调用patchUnkeyedChildren方法,大概有4个步骤:
//源码:
const patchUnkeyedChildren = (
c1: VNode[], //旧的Nodes
c2: VNodeArrayChildren, //新的Nodes
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength) //取新旧Nodes中长度较短的那个进行遍历
let i
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
//新旧Nodes中相同位置上的元素进行patch,如果相同,不做处理,如果不同就更新内容
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
//如果旧Nodes长度大于新Nodes,就把旧Nodes中多余的部分删除
if (oldLength > newLength) {
// remove old
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
// mount new
//如果旧Nodes长度小于新Ndoes,说明旧Nodes要新增一部分元素。
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
6.绑定了key
绑定了key时,就会使用第三种方法,有key会调用patchKeyedChildren方法,这样效率是最高的.
patchKeyedChildrend方法大概有5步:
//源码:
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1.先从新旧Nodes的头部进行遍历:
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
//判断新旧Nodes中相同位置的节点类型和key是否相同,如果相同进行patch
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
//如果不相同退出循环
break
}
i++
}
// 2.再从新旧Nodes的尾部进行遍历
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
//同样判断相同位置处的新旧节点类型和key是否相同,如果相同,进行patch
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
//不同就跳出循环
break
}
e1--
e2--
}
// 3.之后如果是新Nodes长度更长,那么会在旧Nodes中挂载新增的节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null, //n1为null对比就相当于挂载
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4.如果是旧Nodes更长,那么就会在旧Nodes中卸载多余节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
//5.对于既有插入又有删除的情况,会尽量找出类型和key值相同的节点进行复用,其他多余的节点就会进行卸载和挂载操作
else {
...
}
}
\