【vue设计与实现】快速Diff算法 1

快速Diff算法,顾名思义,该算法的实测速度非常快。该算法最早应用于ivi和inferno这两个框架,Vue.js3借鉴并进行了扩展。
不同于简单Diff算法和双端Diff算法,快速Diff算法包含预处理步骤,这其实也是借鉴了纯文本Diff算法的思路。在纯文本Diff算法中,存在对两段文本进行预处理的过程。
例如,在对两端文本进行Diff之前,会先对其进行全等比较,这也叫做快捷路径。如果全等,就不用进入核心Diff算法的步骤。此外,预处理过程还会处理两段文本相同的前缀后缀
两段文本中相同内容,就不需要进行核心Diff操作。这其实是简化问题的一种方式。这样在特定情况下能够判断文本的插入和删除,如下面的例子:

TEXT1: I like you
TEXT2: I like you too

经过比较可以发现,TEXT2是在TEXT1的基础上增加了字符串too。

快速Diff算法借鉴了纯文本Diff算法中预处理的步骤,看下面的例子:
旧子节点:p1,p2,p3
新子节点:p1,p4,p2,p3

可以发现,两组子节点有相同的前置节点p1和相同的后置节点p3和p4
对于相同的前置节点和后置节点,由于在新旧子节点中的相对位置不变,所以不需要移动,但还是要再它们之间打补丁

对于前置节点,可以建立索引j,其初始值为0,用来指向两组子节点的开头
然后开启一个while循环,让索引j递增,直到遇到不相同的节点为止,如下面的patchKeyedChildren函数的代码所示:

function patchKeyedChildren(n1,n2,container){
	const newChildren = n2.children
	const oldChildren = n1.children
	// 处理相同的前置节点
	// 索引j指向新旧两组子节点的开头
	let j = 0
	let oldVNode = oldChildren[j]
	let newVNode = newChildren[j]
	// while循环向后遍历,直到遇到拥有不同key值的节点为止
	while(oldVNode.key === newVNode.key){
		// 调用patch函数进行更新
		patch(oldVNode,newVNode,container)
		// 更新索引j,让其递增
		j++
		oldVNode = oldChildren[j]
		newVNode = newChildren[k]
	}
}

这样就完成了对前置节点的更新。
这里要注意的是,循环终结时,索引j的值为1

接下来处理相同的后置节点。由于新旧两组子节点的数量可能不同,所以需要两个索引newEnd和oldEnd,分别指向新旧两组子节点中的最后一个节点
在这里插入图片描述
然后,再开启一个while循环,并从后向前遍历这两组子节点,直到遇到key值不同的节点为止,如下面代码:

function patchKeyedChildren(n1,n2,container){
	const newChildren = n2.children
	const oldChildren = n1.children
	// 处理相同的前置节点
	// 索引j指向新旧两组子节点的开头
	let j = 0
	let oldVNode = oldChildren[j]
	let newVNode = newChildren[j]
	// while循环向后遍历,直到遇到拥有不同key值的节点为止
	while(oldVNode.key === newVNode.key){
		// 调用patch函数进行更新
		patch(oldVNode,newVNode,container)
		// 更新索引j,让其递增
		j++
		oldVNode = oldChildren[j]
		newVNode = newChildren[k]
	}

	// 更新相同的后置节点
	// 索引oldEnd指向旧的一组子节点的最后一个节点
	let oldEnd = oldChildren.length - 1
	// 索引newEnd指向新的一组子节点的最后一个节点
	let newEnd = newChildren.length - 1

	oldVNode = oldChildren[oldEnd]
	newVNode = newChildren[newEnd]

	// while 循环从后向前遍历,直到遇到拥有不同key值的节点为止
	while(oldVNode.key === newVNode.key){
		// 调用patch函数进行更新
		patch(oldVNode,newVNode,container)
		// 递减oldEnd和nextEnd
		oldEnd--
		newEnd--
		oldVNode = oldChildren[oldEnd]
		newVNode = newChildren[newEnd]
	}
}

在这一步更新操作过后,新旧两组子节点状态如图:
在这里插入图片描述
可以看到最后还遗留一个未被处理的节点p4,可以看到,节点p4是一个新增节点,那么程序是如何得出“节点p4是新增节点的”,这里就需要观察三个索引j,newEnd和oldEnd之间的关系

  1. oldEnd<j 成立,说明在预处理过程中,所有旧子节点都处理完毕
  2. newEnd>=j成立,说明在预处理过后,在新的一组子节点中,仍然有未被处理的节点,这些遗留的节点将被看作新增节点

条件一和条件二同时成立时,说明在新的一组子节点中,存在遗留节点,且这些节点都是新增节点。因此要将其挂载到正确的位置。
在这里插入图片描述
在新的子节点中,索引值处于j和newEnd之间的任何节点都需要作为新的子节点进行挂载。这里,新增节点应该挂载到节点p2对应的真实DOM前面。所以节点p2对应的真实DOM节点就是挂载操作的锚点元素。下面看具体的代码实现:

function patchKeyedChildren(n1,n2,container){
	const newChildren = n2.children
	const oldChildren = n1.children
	// 更新相同的前置节点
	// 省略部分代码

	// 更新相同的后置节点
	// 省略部分代码
	
	// 预处理完毕后,如果满足如下条件,说明j到nextEnd之间的节点应作为新节点插入
	if(j>oldEnd && j<=newEnd){
		// 锚点的索引
		const anchorIndex = newEnd + 1
		// 锚点元素
		const anchor = anchorIndex < newChildren.length?newChildren[anchorIndex].el : null
		// 采用while循环,调用patch函数逐个挂载新增节点
		while(j<=newEnd){
			patch(null,newChildren[j++],container,anchor)
		} 
	}
}

首先计算锚点的索引值(即anchorIndex)为newEnd+1。如果小于新的一组子节点的数量,则说明锚点元素在新的一组子节点中,所以直接使用newChildren[anchorIndex].el作为锚点元素;否则说明索引newEnd对应的节点已经是尾部节点了,这时无须提供锚点元素。有了锚点元素,就可以开启while循环了。

后面再来看看删除节点的情况:
看下面的例子
旧的子节点:p1,p2,p3
新的子节点:p1,p3
同样适用索引j,oldEnd和newEnd来进行标记,

在这里插入图片描述

在进行完预处理后,各个索引的关系如上图
同理遗留的节点可能是多个,也就是索引j和索引oldEnd之间的任何节点都应该被卸载,具体实现如下:

function patchKeyedChildren(n1,n2,container){
	const newChildren = n2.children
	const oldChildren = n1.children
	// 更新相同的前置节点
	// 省略部分代码

	// 更新相同的后置节点
	// 省略部分代码
	
	if(j>oldEnd && j<=newEnd){
		// 省略
	}else if(j>newEnd && j<=oldEnd){
		// j到oldEnd之间的节点应该被卸载
		while(j<=oldEnd){
			unmount(oldChildren[j++])
		}
	}
}

当满足条件j>newEnd && j<=oldEnd时,则开启一个while循环,并调用unmount去逐个卸载这些遗留节点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值