【vue设计与实现】双端Diff算法 2-非理想状况的处理方式

在前面,用的都是比较理想的例子。双端Diff算法的每一轮比较的过程都分为四个步骤。理想的情况是,每一轮比较都会命中四个步骤中的一个,但实际上,并不是所有情况都这么理想,例如下面这个例子

旧的一组子节点:[p1,p2,p3,p4]
新的一组子节点:[p2,p4,p1,p3]

如果尝试按照双端Diff算法的思路来进行第一轮比较时,会发现无法命中四个步骤中的任何一步,也就是:
第一步:旧头部节点p1和新头部节点p2
第二步:旧尾部节点p4和新尾部节点p3
第三步:旧头部节点p1和新尾部节点p3
第四步:旧尾部节点p4和新头部节点p2

在四个步骤的比较过程中,都无法找到可复用的节点,那只能通过增加额外的处理步骤来处理这种非理想情况,也就是尝试看看非头部、非尾部的节点能否复用。具体做法就是,拿新的一组子节点中的头部节点去旧的一组子节点中寻找,如下面代码所示:

while(oldStartIndex <= oldEndIdx && newStartIdx <= newEndIdx){
	if(oldStartVNode.key === newStartVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldStartVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newStartVNode.key){
		// 省略部分代码
	}else{
		// 遍历旧的一组子节点,试图寻找与newStartVNode拥有相同key值的节点
		// idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
		const idxInOld = oldChildren.findIndex(
			node => node.key === newStartVNode.key
		)
	}
}

这样做其实就是,在旧的一组子节点中找到与新的一组子节点的头部节点具有相同key值的节点。
在上面的例子 新子节点的头部节点p2 会在旧子节点的索引为1的位置找到可复用的节点,也就是说更新后,p2应该变成头部节点。所以要将节点p2对应的真实DOM节点移动到当前旧的一组子节点的头部节点p1所对应的真实DOM节点之前,具体实现如下:

while(oldStartIndex <= oldEndIdx && newStartIdx <= newEndIdx){
	if(oldStartVNode.key === newStartVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldStartVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newStartVNode.key){
		// 省略部分代码
	}else{
		// 遍历旧children,视图寻找与newStartVNode拥有相同key值的元素
		const idxInOld = oldChildren.findIndex(
			node => node.key === newStartVNode.key
		)
		// 如果idxInOld大于0,说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
		if(idxInOld > 0){
			// idInOld位置对应的vnode就是需要移动的节点
			const vnodeToMove = oldChildren[idInOld]
			// 移动外还应该打补丁
			patch(vnodeToMove, newStartVNode, container)
			// 将vnodeToMove.el 移动到头部节点 oldStartVNode.el 之前,因此使用后者作为锚点
			insert(vnodeToMove.el, container, oldStartVNode.el)
			// 由于位置idxInOld处的节点所对应的真实DOM已经移动到了别处,因此将其设置为undefined
			oldChildren[idxInOld] = undefined
			// 最后更新 newStartIdx 到下一个位置
			newStartVNode = newChildren[++newStartIdx]
		}
	}
}

这里要注意的是

  1. 由于处于idxInOld处的节点已经处理过了(对应的真实DOM移动了别处),因此应该将oldChildren[idxInOld]设置为undefined
  2. 新的一组子节点中的头部节点已经处理完毕,因此将newStartIdx前进到下一个位置

经过上述两步的操作后,新旧两组子节点以及真实DOM节点的状态如下

旧的一组子节点:[p1,undefined,p3,p4]
新的一组子节点:[‘p2’,p4,p1,p3]

真实的DOM顺序是 p2移动到p1前即 [p2,p1,p3,p4]

双端Diff算法会继续进行:
在第二轮中会发现旧子节点尾部节点p4与新子节点的头部节点p4两者的key值相同,可以复用。此时真实DOM节点的顺序是 [p2,p4,p1,p3]
接着开始下一步的比较,发现在第一步旧子节点的头部节点p1和新子节点的头部节点p1,两者的key值相同,可以复用

由于两者都处于头部,所以不需要对真实DOM进行移动,只需要打补丁即可。
在这里插入图片描述

此时真实DOM节点的顺序是[p2,p4,p1,p3],再进行下一轮比较。要注意的是,此时旧子节点的头部节点是undefined,说明该节点已经被处理了,因此不用再处理了,直接跳过即可。为此,需要补充这部分逻辑的代码,具体实现如下:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
	// 增加两个判断分支,如果头尾部节点为undefined,则说明改节点已经被处理过了,直接跳到下一个位置
	if(!oldStartVNode){
		oldStartVNode = oldChildren[++oldStartIdx]
	}else if(!oldEndVNode){
		oldEndVNode = newChildren[--oldEndIdx]
	}else if(oldStartVNode.key === newStartVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldStartVNode.key === newEndVNode.key){
		// 省略部分代码
	}else if(oldEndVNode.key === newStartVNode.key){
		// 省略部分代码
	}else{
		const idxInOld = oldChildren.findIndex(
			node=>node.key===newStartVNode.key
		)
		if(idxInOld > 0){
			const vnodeToMove = oldChildren[idxInOld]
			patch(vnodeToMove, newStartVNode, container)
			insert(vnodeToMove.el, container, oldStartVNode.el)
			oldChildren[idxInOld] = undefined
			newStartVNode = newChildren[++newStartIdx]
		}
	}
}

上面的代码,在循环开始时,优先判断头部节点和尾部节点是否存在,如果不存在,则说明已经被处理过了,直接跳到下一个位置即可,在这一轮比较过后,新旧两组子节点与真实DOM节点的状态如图:
在这里插入图片描述

接着做最后一轮的比较
比较旧子节点的头部节点p3和新子节点的头部节点p3,两者的key值相同,可以复用

这样在第一步旧找到了可复用的节点。由于都是头部节点,因此不需要进行DOM移动操作,直接打补丁即可,这样就更新完成,最终,真实DOM节点的顺序与新的一组子节点的顺序一致。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值