之前都是用excel记录知识的,现在想想还是记录在网上把。一个是自己的理解有错误的地方可能会有大神帮忙指点一下,另一个就是查找的时候也方便点(我excel跟typora记录的位置太乱了)
前两天看到一道面试题,vue中key的作用。当时我的第一印象就是v-for中key的用法,在VUE官方是这么介绍的:
当 Vue 正在更新使用 v-for渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"。为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性
当时的理解是,假设有个列表的值是1,2,3, 如果列表顺序改变了,变成3,1,2,vue不会重新渲染新的列表,只是更改其index顺序进行更新,这样子就可以节省资源了。这里key值得作用是vue识别节点的机制。注意:key使用字符串或者数值类型。
这样子简单的理解后,我又去官方看了下key的详细介绍。
key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
这里开始,就涉及到VUE虚拟DOM 的diff算法了。刚好也想了解下这个,便去查找了下相关资料。
超链接1
超链接2
超链接3
以上三个网址是我觉得写的蛮详细的,我也是参考了他们的内容进行了自己的理解。
Diff算法
虚拟DOM
简单来说,vue参照真实的DOM会生成一个DOM树,称为Old Vnode,然后我们更新了某个节点,生成了一个新的DOM树,成为Vnode,这里的DOM树就称为虚拟DOM。diff算法比较的就是Vnode与Old Vnode的区别,然后将区别更新到真实DOM里面,同时将Old Vnode变成Vnode。 DOM树其实就是将真实DO面数据抽取出来,以对象形式模拟树结构,如真实DOM假设是:
<div>
<p>123</p>
</div>
对象化为虚拟DOM(这里是简单示例,真实的对象结构请参考vue源码中的DOM树对象)
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
简单说下为何要虚拟DOM
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
关于虚拟DOM,参考的是vue核心之虚拟DOM(vdom)
diff算法的比较方式
- 同层比较,如上面div的Old Vnode,跟其Vnode比较,div只会跟同层div比较,不会跟p进行比较,下面是示例图:
这样子比较的好处是降低了时间复杂度,放弃了深度遍历。不过牺牲了一定的性能。如果两个父元素不同,但是子元素完全相同,父元素匹配到不同,Vnode就会替换掉Old Vnode的元素,并没有实现子元素的复用。 这里的sameVnode方法是用来判断两个节点是否可以比较,如果sameVnode(Vnode,Old Vnode)=false,说明两个节点不同,则Vnode直接替换掉Old Vnode元素
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
同层比较(sameVnode==true),则进行patchVnode比较
\\patchVnode的简单逻辑
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
patchVnode的逻辑是:
- 如果oldVnode跟vnode完全一致,那么不需要做任何事情
- 如果oldVnode跟vnode都是静态节点,且具有相同的key,当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作
- 否则,如果vnode不是文本节点或注释节点:
- 如果oldVnode和vnode都有子节点,且2方的子节点不完全一致,就执行updateChildren
- 如果只有oldVnode有子节点,那就把这些节点都删除 如果只有vnode有子节点,那就创建这些子节点
- 如果oldVnode和vnode都没有子节点,但是oldVnode是文本节点或注释节点,就把vnode.elm的文本设置为空字符串如果vnode是文本节点或注释节点,但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
updateChildren函数
这个函数比较复杂,先贴下代码
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
整个代码的逻辑:
-
将Vnode的头尾取出跟OldVnode的头尾取出进行四次比较
取出OldVnode的头尾os ,oe 取出Vnode的头尾vs,ve,比较:
case 1:if(os=vs){
说明两个node的头相同,那么将两者的头向后跳一格(即os原本是OlaVnode[0],变 成OldVnode[1])}
case 2:if(os=ve){
说明OldVnode的尾应该是新的头,则将ve移到OldVnode的最前面 }
case 3:if(vs=oe){
说明OldVnode的头应该是新的尾,则将ve移到OldVnode的最后面 }
case 4:if(ve=oe){
说明两个node的尾相同,那么将两者的尾向前跳一格(即oe原本是OldVnode[length-1],变 成OldVnode[length-2])}
}
case5:if(无法匹配){
那么vs/ve 遍历oldVnode每一项
if(samekey-如果两者Key相同){
if(samenode){
将oldvnode匹配到的元素移到对应的vs/ve的位置
}else{
说明此元素为新元素,OldVnode添加vs/ve元素,移到对应的vs/ve位置
}
}else{ 说明此元素为新元素,OldVnode添加vs/ve元素,移到对应的vs/ve位置
} } -
示例
假设OldVnode为a,b,c,d
Vnode为a,c,d,e,b
os=a,oe=d,vs=a,vs=b
第一次遍历:case1为true,则将两者的头向后跳一格,此刻OldVnode为b,c,d , Vnode为c,d,e,b ,os=b,oe=d,vs=c,ve=b
第二次遍历:case2为true,则将OldVnode的b元素移至最后,此刻OldVnode为c,d,b ,这样子就是case4为true,则将两者的尾向前跳一格。此刻OldVnod为c,d ,Vnode为c,d,e
第三次遍历:重复上述步骤,经过两次循环后,最终OldVnode先循环结束,Vnode还剩个e,则e为新元素,直接根据自身Index插入OldVnode -
关键点
key值的作用:在进行比较的时候,如果头尾匹配不到,则会遍历整个OldVnode,进行判断是否存在Key值相同的元素进行复用,最大化的利用了组件
如果循环中,OldVnode先循环完,Vnode还未循环完,说明Vnode比OldVnode多了未循环的元素,直接插入,反之,说明OldVnode比Vnode多了未循环的元素,直接移除。
PS
这是我一个真前端小白的一些简单的理解,对于源码部分我也只是仔细看了下UpdateChildren函数,还有很多地方需要去理解,这是第一次发表文章,排版有问题请谅解- -如果有大佬发现有问题求指教( •́ .̫ •̀ )