snabbdom patch函数的简单实现 diff算法精细化比较实现

从上一篇文章Diff算法的关键,现在来看看patch函数如何讲虚拟节点更新到DOM树上。以及如何进行diff算法。

看一张图在这里插入图片描述
可以看下源码:
在这里插入图片描述

完成简单版patch

先看基本形状
在这里插入图片描述
在这里插入图片描述
使用
在这里插入图片描述
在这里插入图片描述
注意,当我们的虚拟节点的children是数组是,那必须递归继续调用createElement,而我们现在写的createElement需要第二个参数DOM节点,递归的时候我们是没有这个节点的,而是一个一个直接加上去的,所以需要改造一下。
在这里插入图片描述
在这里插入图片描述
在外面做处理。
而递归的话我们就可以在CreateELement里面直接上树

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
正常上树。

这个patch函数的简单上树其实跟ReactDom.render函数差不多,都是判断有没有children来做一个处理,有则继续递归。

在验证一下不同节点是否暴力删除

在这里插入图片描述在这里插入图片描述

不同节点的替换已经完成,接着准备开始动手最难的diff算法精细化比较部分。

精细化比较

先按一张图
在这里插入图片描述
如果是
如果是同一节点,就判断是不是同一对象。再判断new是否有text,节点,有的话就表示没有children,(低配版,假设只能传children或者text),那么就判断老的节点的text是否与新节点相同,如果不相同就表明老节点的text跟新节点的text不一样,或者老的节点有children,这时候我们直接将新节点的text直接替换老节点的内容,里面不管是text,还是children都会被替换掉。
那么新节点没有text时,就表示有children,在判断老节点有没有children,有的话就是我们最最最复杂的操作了。没有的话就容易了,直接暴力删除老节点的txt,然后将新节点的DOM插入即可。

我们先来完成简单的两步操作。
在这里插入图片描述
当新节点的text不同于老节点时,直接替换。
在这里插入图片描述
那么当新节点不是text时,则表明有children。那么当老节点没有children却有text时
在这里插入图片描述
就直接将text值去掉,然后递归创建节点,放入老节点的DOM中。

接着到最复杂的地方了,就是两个都有children,在这里插入图片描述
有没有发现一个逻辑,就是它的内容可能会出现层层递进的,就是一层里面还有一层,一层里面还有一层。所以我们做判断的时候,必须,每一层都进行重头判断,从一开始老节点是不是DOM的时候开始,一直判断,判断到它里面还有h函数,又要递归进行判断,等等

这里有三种情况,第一种直接增加

在这里插入图片描述

第二种 更新,就是A变成C

第三种,减少。比如将B去掉。

一般人可能会用两个for循环遍历对比两个children的改变(包括我),比如第一种的情况

在这里插入图片描述
我们就要通过老的children来帮忙了,比如我们遍历到AB,然后第三个不一样,按道理是要插入到C前面,所以我们就用一个指针指向老的children,若有相同的节点则继续加,没有的话证明现在要加的节点,都应该在我们老children未处理的节点前面。那么当我们的指针走完了,表明我们接下来创建的节点在children后面,所以直接添加就行。
但是第二种第三种的话写进去就太麻烦了,这种算法太low,接下来看比较实用的。

接下来是DIFF算法的一个更新策略,运动四个指针。也是面试官经常喜欢问的。

语言描述说不太清,推荐去B站看下diff算法。
这是网址:https://www.bilibili.com/video/BV1v5411H7gZ?p=12&spm_id_from=pageDriver
这里只是总结下。
四个指针。分别是新前,新后,旧前,旧后,可以看下源码
在这里插入图片描述
意思就是新

新前指向新的节点的第一个节点,

新后新节点的最后一个节点

旧前指向旧节点的第一个指针。

旧后指向的是旧节点的最后一个指针。

一共是四种判断方法。
分别是

新前与旧前

新后与旧后

新后与旧前

新前与旧后。

一共有六种情况,前面四种,加for循环找到了,for循环没找到两种。

什么意思呢,就是新前指针跟新后指针相比较,如果是同个节点,就一起指向下一个节点。
同理,新后与旧后相比较,是同一个节点是,就一起指向上一个节点。
新后与旧前是同一个节点时,新后向上,旧前向下。
新前与旧后是同一个节点时,新前向下,旧后向下。
当四种都没有的情况呢,就用for循环。也不算for循环,内部会调用一个函数根据key值与索引值生成一个对象。通过新前指针的key值直接拿到对象里面的数据,若返回undefined则为第六种,都没找到。
若找到了则为第五种,通过for循环找到。

记住,每个节点相互比较时,总是从第一个就是新前与旧前,开始
比到最后一个的。
什么时候循环结束呢。
就是当新前小于等于新后时,旧前小于等于旧后时,循环继续。
当新节点的指针先结束,就是新前大于新后时,此时旧前跟旧后包含的节点啥的就要删除。
当旧节点的指针先结束时,就是旧前大于旧后,此时新前与新后包含的节点就要被加入到最后面。
第三第四的情况还要额外处理。
第三则新后与旧前相比较,匹配到的时候,就要将新前指向的节点,挪到旧后之后的节点。然后再各自向上向下移。
第四则是新前与旧后匹配到的时候,就要将新前指向的节点,放到旧前之前的位置,然后在各自向上向下挪。
for循环只有一种情况出现,就是当四种都不匹配,即判断到新前与旧后不匹配时,进行for循环。for循环是从旧前跟旧后之间循环的
一种是,通过for在老节点找到了,那么就将新前的节点在真实DOM上插入到旧前前面,再在老节点里面对匹配到的节点设为undefined.。然后新前向上诺,旧前旧后不用动。
一种是找不到的情况下。就将新前指向节点放到旧前前面,然后继续新前向上走。旧前或旧后不用动,因为没有匹配到。
在这里插入图片描述
可以看源码也是这种比较类型。

我们可以先来写在前面四种类型

在这里插入图片描述
首先循环判断
在这里插入图片描述
第一种,新前旧前,这个的精妙之处就是会调用patchVnode,然后去处理节点,将老节点的值更新到新节点上去。而当两个比较的节点,都有children时,patchVnode里面会调用upDateChildren,互相调用,只有两个节点的子为children才会继续调用upDateChildren,所以就会一直遍历,一直调用,知道不是children。还要记得挪动指针。
在这里插入图片描述
第二种,匹配到的时候,就用patchVnode处理节点,即最小精良化比较,在老节点上进行操作。
在这里插入图片描述
新后旧前就要注意了,需要进行节点操作,由于是同一个节点,所以老的和新的的DOM元素是一样的即ELM是一样的,children不同而已。patchVnode就是将新的节点的变化反映到老节点上,比如text的改变,因为patchVnode是对老节点的DOM进行操作的嘛,然后再将该老节点插入到旧后前后面。因为该老节点进行patchVnode后,连children就跟新节点一样,所以直接将该节点插入到旧后后面。
在这里插入图片描述

新前与旧后,思路一样的。

接着再写当跳出while的时候,for循环那块稍后再写。

在这里插入图片描述
这个还是比较容易的,新的先结束,就直接遍历删除老的,老的先结束,就直接遍历新增那些新的。这里用Fragment是可以减少DOM操作,文档碎片流对象,不懂得可以去百度。

接着写最复杂的

就是for循环,
我们可以看下源码

在这里插入图片描述
在这里插入图片描述
这个是做什么的,就是将我们的key值存起来,就是旧前旧后中包含的Key值取出来,方便我们循环找到,而不用每次都去for,
在这里插入图片描述
我们可以简单实现一下
在这里插入图片描述
当我们开始for循环时,就直接取出旧前酒后中包含的key值,然后我们可以通过KEYMAP[新前缩影]拿到这个值,而不用去遍历,为undefined时,则表示没找到,不为Undefined时,表示找到了,这时候就要从oldCh中拿出这个节点,然后将该节点更新为新节点,就是PatchVnode处理,然后再将该项设位undefined,再把找到的这个节点挪到旧前前面。大功告成。只不过我们要在循环前先跳过undefined,
在这里插入图片描述
避免没必要的判断。
现在我们已经处理了前面四种加for循环找到的情况,就剩下for循环没找到的情况。

在这里插入图片描述
这个更容易,直接在旧前前面加上我们创建的指针,因为新创建的,老节点没有这个,所以不用patchVnode来处理节点,直接创建一个新的就行。在这里插入图片描述
记住,不管找没找到,指针都向下移。这里写错了,在这里插入图片描述
不用加.elm,因为CreatElement是接受整个节点的,不是接受DOM元素。

接下来在这里插入图片描述
完美运行。
我们写的是一个简单版的,所以没有处理它的属性,要的话直接在createElement种遍历data然后通过setAttrbute来设置属性值。

总结

精细化比较的实现,就是通过四个指针,加上for循环(实际用keymap代替了,性能更优化),一共有六种情况。
即 新前旧前,新旧旧后,若这两种遍历到了,就直接处理新老节点的数据,如patchVnode(old, new)
就会讲new的数据更新到old上,然后再指针该往哪走往哪走。
接着就是 新后旧前,新前旧后,这两种若是匹配到了,除了要patchVnode(old,new),新后旧前的话,是需要将新前的节点插入在旧后后面,新前旧后的话就是将新前节点插到旧前的前面。记住,都是新前去插,然后第三种就是插到旧后后,第四种就是插到旧前前。然后再挪动指针。
接着就是四种都没找到。启动KeyMAP查询。
如果找不到,那就容易了,直接createElement创建新节点,插在旧前前面。
如果找到了,那就麻烦一点,就是取出找到的老的节点,通过keyMap[key]拿到这个节点,然后更新数据,patchVnode,再将老节点上该位置设为undefined,因为这个已经要移动了,最后就直接插入到旧前前面。
不管找不找得到,就是插入旧前前面,然后新前继续往下挪。

还有一个就是patchVnode与upDateChildren的巧妙之处,互相调用。
patchVnode是用来更新数据的,比如将新节点的text/children更新到老节点的text/children,而当老节点和新节点都有children时,证明他们的children里还有节点。这时候就要调用upDateChildren了,调用后,在里面对children做判断,更新数据又要用到patchVnode,互相调用。而他们的互相调用停止的关键就是,当children中的子节点中的children为空,就是没有children,这时候patchVnode就会停止调用upDateChidlren,而是将直接更新节点了。

猜你喜欢

转载自blog.csdn.net/lin_fightin/article/details/114648971