之前通过减少DOM操作的次数,提升了性能,但是还是有可优化的空间,例如,假设新旧两组子节点的内容如下:
// oldChildren
[
{type:'p'},
{type:'div'},
{type:'span'}
]
// newChildren
[
{type:'span'},
{type:'p'},
{type:'div'}
]
使用之前的算法来的话,需要6次DOM操作,因为类型不同,需要卸载旧节点后再加载新节点,这样每更新一个节点就需要两次DOM操作。
但是通过观察可以发现,两组节点只是顺序不一样,所以最优的处理方式是,通过DOM的移动来完成子节点的更新。但是想要通过DOM的移动来完成更新,必须要保证一个前提,新旧两组子节点中的确存在可复用的节点。那么,应该如何确定新的子节点是否出现在就的一组子节点中,有一种方案是通过vnode.type来判断,只要vnode.type的值相同,就认为是相同的节点,但是这样不靠谱,如果vnode相同,而children不同呢。那么可以引入额外的key来作为vnode的标识,如下面代码:
// oldChildren
[
{type:'p',children:'1',key:1},
{type:'p',children:'2',key:2},
{type:'p',children:'3',key:3},
]
// newChildren
[
{type:'p',children:'3',key:3},
{type:'p',children:'1',key:1},
{type:'p',children:'2',key:2},
]
这样只要两个虚拟节点的type属性值和key属性值都相同,那么就认为它们是相同的,即可以进行DOM复用。
当然DOM可复用并不意味这不需要更新,如下面两个虚拟节点:
const oldVnode = {type:'p',key:1,children:'text 1'}
const oldVnode = {type:'p',key:1,children:'text 2'}
这意味着,在更新时可以复用DOM元素,仍然需要对这两个虚拟节点进行打补丁操作。因此,在讨论如何移动DOM之前,需要先完成打补丁的操作,如下面patchChildren函数的代码所示:
function patchChildren(n1,n2,container){
if(typeof n2.children === 'string'){
// 省略部分代码
}else if(Array.isArray(n2.children)){
const oldChildren = n1.children
const newChildren = n2.children
// 遍历新的children
for(let i=0;i<newChildren.length;i++){
const newVNode = newChildren[i]
// 遍历旧的children
for(let j = 0; j<oldChildre.length; j++){
const oldVNode = oldChildren[j]
// 如果找到了具有相同key值的两个节点,说明可以复用,但仍然需要调用patch函数进行更新
if(newVNode.key === oldVNode.key){
patch(oldVNode,newVNode,container)
break
}
}
}
}else{
/.../
}
}
这样就能保证所有可复用的节点本身都已经更新完毕,以下面的新旧两组子节点为例:
const oldVnode = {
type: 'div',
children: [
{type:'p',children:'1',key:1},
{type:'p',children:'2',key:2},
{type:'p',children:'3',key:3},
]
}
const newVnode = {
type: 'div',
children: [
{type:'p',children:'world',key:3},
{type:'p',children:'1',key:1},
{type:'p',children:'2',key:2},
]
}
// 首次挂载
renderer.render(oldVnode,document.querySelector('#app'))
setTimeout(()=>{
// 1秒后更新
renderer.render(newVnode, document.querySelector('#app'))
},1000)
经过上面的更新操作后,所有节点对应的真实DOM元素都更新完毕了。但真实DOM仍然保持就的一组子节点的顺序,因此还需要通过移动节点来完成真实DOM顺序的更新