详解key在Vue列表渲染时究竟起到了什么作用
Vue2+采用diff算法来进行新旧vnode的对比从而更新DOM节点。而通常在我们使用v-for这个指令的时候,Vue会要求你给循环列表的每一项添加唯一的key,那么这个key在渲染列表时究竟起到了什么作用呢?
在解释这一点之前,你最好已经了解Vue的diff算法的具体原理是什么。
Vue2更新真实DOM的操作主要是两种:创建新DOM节点并移除旧DOM节点和更新已存在的DOM节点,这两种方式里创建新DOM节点的开销肯定是远大于更新或移动已有的DOM节点,所以在diff中逻辑都是为了减少新的创建而更多的去复用已有DOM节点来完成DOM的更新。
在新旧vnode的diff过程中,key是判断两个节点是否为同一节点的首要条件:
//参见Vue2源码core/vdom/patch.js functionsameVnode(a,b){ return( a.key===b.key&&( ( a.tag===b.tag&& a.isComment===b.isComment&& isDef(a.data)===isDef(b.data)&& sameInputType(a,b) )||( isTrue(a.isAsyncPlaceholder)&& a.asyncFactory===b.asyncFactory&& isUndef(b.asyncFactory.error) ) ) ) }
值得注意的是,如果新旧vnode的key值都未定义的话那么两个key都为undefined,a.key===b.key是成立的
接下来是在updateChildren方法中,这个方法会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM
//参见Vue2源码core/vdom/patch.js functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){ ... while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){ if(isUndef(oldStartVnode)){ ... }elseif(isUndef(oldEndVnode)){ ... }elseif(sameVnode(oldStartVnode,newStartVnode)){ ... }elseif(sameVnode(oldEndVnode,newEndVnode)){ ... }elseif(sameVnode(oldStartVnode,newEndVnode)){//Vnodemovedright ... }elseif(sameVnode(oldEndVnode,newStartVnode)){//Vnodemovedleft ... }else{ if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx) idxInOld=isDef(newStartVnode.key) ?oldKeyToIdx[newStartVnode.key] :findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx) if(isUndef(idxInOld)){//Newelement createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx) }else{ vnodeToMove=oldCh[idxInOld] if(sameVnode(vnodeToMove,newStartVnode)){ patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx) oldCh[idxInOld]=undefined canMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm) }else{ //samekeybutdifferentelement.treatasnewelement createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx) } } newStartVnode=newCh[++newStartIdx] } } ... }
设置key的可以在diff中更快速的找到对应节点,提高diff速度
在updateChildren方法的while循环中,如果头尾交叉对比没有结果,即oldStartVnode存在且oldEndVnode存在且新旧children首尾四个vnode互不相同的条件下,会根据newStartVnode的key去对比oldCh数组中的key,从而找到相应oldVnode
首先通过createKeyToOldIdx方法创建一个关于oldCh的map
if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx) functioncreateKeyToOldIdx(children,beginIdx,endIdx){ leti,key constmap={} for(i=beginIdx;i<=endIdx;++i){ key=children[i].key if(isDef(key))map[key]=i } returnmap }
这个map中将所有定义了key的oldVnode在数组中的index值作为键值,它的key作为键名存储起来,然后赋给oldKeyToIdx
idxInOld=isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx) functionfindIdxInOld(node,oldCh,start,end){ for(leti=start;i如果newStartVnode的key存在的话,就去oldKeyToIdx中寻找相同key所对应的index值,这样就能拿到跟newStartVnode的key相同的oldVnode在oldCh数组中的index,即得到了与newStartVnode对应的oldVnode。如果找不到的话,那么idxInOld就为undefined。
而如果newStartVnode并没有设置key,则通过findIdxInOld方法遍历oldCh来获取与newStartVnode互为sameVnode的oldVnode,返回这个oldVnode在oldCh数组的index。(前面介绍过,Vue在更新真实DOM时倾向于真实DOM节点的复用,所以在这里还是会选择去找对应的oldVnode,来更新已有的DOM节点)
这时候设置key的好处就显而易见了,有key存在时我们可以通过map映射快速定位到对应的oldVnode然后进行patch,没有key值时我们需要遍历这个oldCh数组然后去一一进行比较,相比之下肯定是key存在时diff更高效。
接下来就是更新DOM的过程,如果oldCh[idxInOld]存在且与newStartVnode互为sameVnode存在则先更新再移动,否则创建新的element
if(isUndef(idxInOld)){//Newelement createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx) }else{ vnodeToMove=oldCh[idxInOld] if(sameVnode(vnodeToMove,newStartVnode)){ patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx) oldCh[idxInOld]=undefined canMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm) }else{ //samekeybutdifferentelement.treatasnewelement createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx) } }那么设置key值就一定能提高diff效率吗?
答案是否定的
`{{i}}
我们给数组设置了key之后数组的diff效率真的变高了吗?
并没有,因为在简单模板的数组渲染中,新旧节点的key都为undefined,根据sameVnode的判断条件,这些新旧节点的key、tag等属性全部相同,所以在sameVnode(oldStartVnode,newStartVnode)这一步的时候就已经判定为对应的节点(不再执行头尾交叉对比),然后直接进行patchVnode,根本没有走后面的那些else。每一次循环新旧节点都是相对应的,只需要更新其内的文本内容就可以完成DOM更新,这种原地复用的效率无疑是最高的。
而当我们设置了key之后,则会根据头尾交叉对比结果去执行下面的ifelse,进行判断之后还需要执行insertBefore等方法移动真实DOM的节点的位置或者进行DOM节点的添加和删除,这样的查找复用开销肯定要比不带key直接原地复用的开销要高。
Vue文档中对此也进行了说明:
当Vue.js用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出。
建议尽可能在使用v-for时提供key,除非遍历输出的DOM内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
所以,简单列表的渲染可以不使用key或者用数组的index作为key(效果等同于不带key),这种模式下性能最高,但是并不能准确的更新列表项的状态。一旦你需要保存列表项的状态,那么就需要用使用唯一的key用来准确的定位每一个列表项以及复用其自身的状态,而大部分情况下列表组件都有自己的状态。
总结
key在列表渲染中的作用是:在复杂的列表渲染中快速准确的找到与newVnode相对应的oldVnode,提升diff效率
以上所述是小编给大家介绍的key在Vue列表渲染时究竟起到了什么作用详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!