现在我们能够通过key值找到可复用的节点,但是如何判断一个节点是否需要移动,以及如何移动。对于第一个问题,可以先这么想:在什么情况下节点不需要移动?其实很简单,当新旧两组子节点的节点顺序不变时,就不需要额外的移动操作。
更新算法在进行时,每一次寻找可复用的节点时,都会记录该可复用节点在旧的一组子节点的位置索引。
如果发现旧的子节点和新的子节点位置索引不同,则节点是需要移动的。其实可以将索引值最大的节点在旧children中的索引定义为:在旧children中寻找具有相同key中,遇到的最大索引值。如果在后续寻找的过程中,存在索引值比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动。
可以用lastIndex变量存储整个寻找过程中遇到的最大索引值,代码如下:
function patchChildren(n1,n2,container){
if(typeof n2.children === 'string'){
// ...
}else if(Array.isArray(n2.children){
const oldChildren = n1.children
const newChildren = n2.children
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
for(let i=0;i<newChildren.length;i++){
const newVNode = newChildren[i]
for(let j=0;j<oldChildren.length;j++){
const oldVNode = oldChildren[j]
if(newVNode.key === oldVNode.key){
patch(oldVNode,newVNode,container)
if(j<lastIndex){
// 如果当前找到的节点在旧children中的索引小于最大索引值lastIndex
// 说明该节点对应的真实DOM需要移动
// lastIndex从0开始
}else{
// 如果当前找到的节点在旧children中的索引不小于最大索引值lastIndex
// 则更新lastIndex的值
lastIndex = j
}
break
}
}
}
}
}
如何移动元素
移动节点指的是,移动一个虚拟节点所对应的真实DOM节点。注意移动的是真实节点,要移动真实DOM节点,就需要取得对其的引用才行。而当虚拟节点被挂载后,其对应的真实DOM节点会存储在vnode.el属性中。因此在代码中可以通过旧子节点的vnode.el属性来取得对应的真实DOM节点。
而在patchElement函数中首先就讲旧节点的n1.el属性赋值给新节点的n2.el属性。
const el = n2.el = n1.el
这个赋值语句的真正含义其实就是DOM元素的复用。在这之后,新节点也有对真实DOM的引用
其实新children的顺序其实就是更新后真实DOM节点应有的顺序
这样就可以开始着手实现代码,如下所示:
function patchChildren(n1,n2,container){
if(typeof n2.children === 'string'){
// ...
}else if(Array.isArray(n2.children){
const oldChildren = n1.children
const newChildren = n2.children
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
for(let i=0;i<newChildren.length;i++){
const newVNode = newChildren[i]
for(let j=0;j<oldChildren.length;j++){
const oldVNode = oldChildren[j]
if(newVNode.key === oldVNode.key){
patch(oldVNode,newVNode,container)
if(j<lastIndex){
// 先获取newVNode的前一个vnode,即preVNode
const preVNode = newChildren[i-1]
// 如果preVNode不存在,则说明当前newVNode是第一个节点,不需要移动
if(preVNode){
// 由于要移动到preVNode对应真实DOM后面
// 所以需要获取preVNode所对应真实DOM的下一个兄弟节点,并将其作为锚点
const anchor = preVNode.el.nextSibling
// 调用insert方法将newVNode对应的真实DOM插入到锚点元素前面
// 也就是prevVNode对应真实DOM的后面
insert(newVNode.el, container, anchor)
}
}else{
// 如果当前找到的节点在旧children中的索引不小于最大索引值lastIndex
// 则更新lastIndex的值
lastIndex = j
}
break
}
}
}
}
}
要注意的是这里insert函数依赖浏览器原生的insertBefore函数