Vue中key为index和id的区别示例详解
作者:Lakeiedward
一、Diff算法
在了解key的作用之前,先简单认识一下diff算法👇
diff算法的特点是平级比较,采用双指针和递归的方式进行逐级比较。
Vue会有一个根节点,先判断根节点是否是文本节点,如果不是文本节点,则会判断是否都有儿子节点,如果都有并且新旧儿子节点不相等,此时就会比较这两个新旧儿子节点(updateChildren),在做比较的时候会有以下几种情况:
- 头头对比
- 尾尾对比
- 头尾对比
- 尾头对比
- 乱序对比,根据旧节点会生成一个映射表(也就是map对象),用新的节点一个个在映射表里找,没有的话插入,有的话移动复用,多余的删掉。
二、Key的作用
在比较两个节点的时候sameVnode(oldStartVnode, newStartVnode)
,主要根据key进行判断两个元素是否是一个元素,key不相同的话则说明不是同一个元素。使用key的时候尽量保持key的唯一性(这样可以优化diff算法)
动态列表添加的key的时候,要避免使用索引(index)!
接下来,我们使用数组渲染一组儿子节点小li,并且通过事件在数组的头部增加(unshift)一个数据;当key为index的时候,我们查看下图图片渲染的情况发现所有的小li都变化了,而key为id的时候,则只在li的最前面新加了一个小li,这就是diff算法根据key判断产生的差异性,具体在下面来看一看。
三、Key为Index
1) 图解
如下图,首先上面是旧节点,下面是新节点,新节点上在数组最前面新加了一个C节点,因为key是index,所以此时C的key还是0,但是文本是C,并不是A。
因为第一个新旧节点的key相同,所以此时会先进入到头头对比中,而不会进入到尾尾对比,在对比的过程中,会再次进入到patchVnode方法
中判断新旧节点的文本是否一致,如果一致则直接复用,不一致则会对dom进行操作,将旧节点文本替换成新节点文本node.textContent = text
第一组对比完成之后,新旧节点的索引会依次增加,对比第二组,第二组的key也是一样的,会重复第一组的对比方式,最后将旧节点文本替换成新节点文本node.textContent = text
此时因为旧节点的开始索引和结束索引相等,则会退出while循环,根据判断新旧节点的开始和结束索引得出,最后一个剩余的新节点会插入(addVnodes
)到A元素后面去。
此时更新就结束了,会发现进行了三次dom操作,虽然新旧节点除了新增的C节点,其他都是相同的,但是都没有复用原来的节点,而是直接使用textContent
改变文本,所以index作为key不中!
2) 完整的步骤
看下一个完整的步骤:
- 如果key是index,在头部添加一个节点,新加的节点key还是0,和第一个旧节点是一样的key(但是文本不一样),sameVnode就会判断他们俩是一样的节点,就会头头对比(而不是尾尾对比),此时虽然key相同的了,但是会递归进入到patchNode中时,会判断文本是否相同(key为index时,文本不相同),如果不相同,则会进行dom文本替换,把旧的文本替换成新的文本,就会出现上图所有的小li进行更新。
- 以上步骤会一直重复头头对比,虽然每次对比时,key都是一样的,但是文本内容不一样,则会一直触发dom更新操作,也就是类似
lis[0].textContent = 'C'
,一直到循环结束oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
,此时把多的新节点添加(addVnodes
)进去,或者多的老节点删除(removeVnodes
)掉。
四、Key为Id
1) 图解
如下图,新加的节点key为c,当进行sameVnode(oldStartVnode, newStartVnode)
对比的时候,发现key不一样
则开始尾尾对比sameVnode(oldEndVnode, newEndVnode)
,此时key是一样的,则进入到patchVnode方法
,判断新旧节点的文本是否一致,一致的话,就复用原来的节点了
对比完第一组,此时新旧节点的尾索引减1,还是尾尾相等,开始尾对比,重复上述的步骤,复用原来的旧节点,没有dom操作。
>
对比完第二组,旧节点的头索引和尾索引相等,则结束while循环,最后一个剩余的新节点会插入(addVnodes
)到A元素前面去。
以上的步骤完成之后,只有最后一次执行了插入dom操作,优化了diff算法和减少了dom操作
2) 完整的步骤
完整的步骤:
- 如果key是唯一的id,向前追加一个,
sameVnode
判断新旧节点时发现新旧节点的key不相同,开始尾对比,尾对比会进入到patchVnode方法
,当为文本节点时,判断新旧节点的文本是否相同,结果发现相同,则不做更新dom操作,直接复用原来的,一直到循环结束oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
,此时只需要把多的新节点添加(addVnodes
)进去,或者多的老节点删除(removeVnodes
)掉即可,没有多余的dom操作。
五、源码
粘贴一下部分的Vue源码
1)sameVnode
只会判断key、 tag、是否有data的存在、是否是注释节点、是否是相同的input type,来判断是否可以复用这个节点。
function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))) ) } function sameInputType(a, b) { if (a.tag !== 'input') return true let i const typeA = isDef((i = a.data)) && isDef((i = i.attrs)) && i.type const typeB = isDef((i = b.data)) && isDef((i = i.attrs)) && i.type return typeA === typeB || (isTextInputType(typeA) && isTextInputType(typeB)) }
2)patchVnode
如果新 vnode 不是文字 vnode
- 那么就要开始对子节点 child 进行对比了。
如果新旧 children 都存在(都存在 li 子节点列表,进入 )
- 那么就是 diff算法 想要考察的最核心的点了,也就是新旧节点的 diff 过程。
如果有新 children 而没有旧 children
- 说明是新增 child,直接 addVnodes 添加新子节点。
如果有旧 children 而没有新 children
- 说明是删除 child,直接 removeVnodes 删除旧子节点
如果新 vnode 是文字 vnode
- 就直接调用浏览器的 dom api 把节点的直接替换掉文字内容就好。
function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly?: any ){ ... // 判断新节点是不是text节点 if (isUndef(vnode.text)) { // 不是text节点 if (isDef(oldCh) && isDef(ch)) { // 老节点和新节点都有child,并且child不相等 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 新节点有child,老节点没有,则新增 if (__DEV__) { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 老节点有child,新节点没有,则删除 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 是text节点并且文本不一样,就把旧的文本替换成新的文本 nodeOps.setTextContent(elm, vnode.text) } ... }
Tips: 儿子节点不是文本时,一方有儿子,一方没有儿子(删除、添加),两方都有儿子,则进入diff算法对比
六、总结
- 动态列表添加的key的时候,要避免使用索引(index)
- 使用唯一的key可以优化diff算法,减少更新dom的操作