Diff算法与最长递增子序列

全文所有算法,都只针对新旧两棵树的某一级。

在React中,是diff旧FiberNode树中某一级的单向链表,和新JSX树中某一级Children数组。

求最长递增子序列

理解最长递增子序列,对理解Diff算法很重要。
React 和 Vue3 的快速排序,都用到了最长递增子序列。
不过,Vue3的快速排序中明确的进行了求最长递增子序列的操作

React则只是在遍历过程中,用一个变量,记录索引位置,来判断当前遍历到的节点是否在最长递增子序列中。

求解过程:

  1. 创建一个数组,数组中的每一项代表一个新节点。每一项的值代表这个节点在旧节点中的位置。
  2. 求这个数组的最长递增子序列。
  3. 那么,这个子序列所代表的那些节点,在更新前后,位置的先后关系,没有变化。
  4. 于是只需要移动或者删除剩下的节点即可。

React

React的diff算法,是对比当前的fiberNode树,和新生成的JSX树。 为方便行文,下文都统称为旧节点新节点

React的diff算法分为

  • 单节点Diff: 新节点为单节点
  • 多节点Diff: 新节点为多节点

单节点Diff

这种情况比较简单。

遍历旧节点

  1. 找到key和type相同的,复用旧节点创建新的fiberNode
  2. 对key或者Type不同的旧fiberNode标记删除
  3. 如果没有找到可复用的旧节点,则标记删除所有旧节点,标记挂载新节点

多节点Diff

有一个前提,大部分情况下节点的位置不会改变。

所以会首先处理位置没有改变的情况。

会遍历两遍。

第一遍找到位置没变的所有节点

第二遍处理其他位置改变了的节点。

第一遍:

同时遍历新旧节点。

声明一个变量:lastPlacedIndex = 0;

从头,也就是索引为0处,开始遍历。 检查新旧节点,相同索引处,节点是否可以复用。

每次循环会更新lastPlacedIndex变量。

  1. 如果可以复用。 lastPlacedIndex++;
  2. 如果key不同,会直接结束第一遍循环
  3. 如果key相同但是type不同,会标记旧fiberNode为DELETION,然后继续循环。

第二遍:

四种情况

  1. 新旧节点都遍历完了。 diff结束。
  2. 旧节点有剩余,新节点遍历完了。标记删除所有剩下的旧节点。
  3. 旧节点遍历完了,新节点还有剩余。新增所有剩余新节点。
  4. 都没有遍历完。这时要处理节点移动。

情况四

最复杂的是情况四

首先用一个键值对,把所有就节点按key保存下来。

然后,遍历新节点。找到每个新节点是否有对应的旧节点。

如果没有:则新增这个节点

如果有:

找到这个旧节点的索引值。 这个索引值代表新节点在旧节点中的位置。

  1. 如果这个索引值大于或等于lastPlaceIndex则不需要移动。 并更新lastPlacedIndex为这个索引值。

  2. 如果小于lastPlacedIndex, 则代表需要移动。

这里乍看之下很难理解。 其实这是在求一个递增子序列。 如果在上面这一系列操作中,如果某个新节点在旧节点中的索引,大于lastPlacedIndex。这些大于lastPlacedIndex的节点们,在更新前后的顺序没有变化。 所以不需要移动他们,而应该去移动那些位置发生过变化的节点。 这个思想在Vue3的快速Diff算法中也有应用。

Vue

在Vue中,diff算法对比的两边都是虚拟Dom。

简单 diff 算法

双循环遍历新旧节点。

外层循环是新节点 记作 new,

内层循环是旧节点:记作 old

假设当前外层循环,循环到了第i次

步骤

声明一个变量: lastIndex = 0;
这里使用到的思想和React diff算法一样。

  1. 拿到 new[i].key,在内层循环中找 old 中相同 key 的索引:index
    1. 如果 index>= lastIndex. 代表 new[i]不需要移动
    2. 如果 index< lastIndex. 代表 new[i]需要移动。
      1. 移动到 new[i-1]后面。
    3. 如果 index 不存在的。代表 new[i]是新增的节点.
      1. 挂载到 new[i-1]后面
  2. 此时双循环遍历完毕。
  3. 遍历 old,去 new 中找对应的 key 是否存在
    1. 如果不存在,代表 old[i]需要被卸载。

双端 diff

从新旧节点,两条链的两端同时开始比较。

每循环完一次,都假设被匹配上处理过之后的节点不存在

于是,可一系列重复的子问题,类似高中的数学归纳法,一个复杂的大问题分解成一步一步,最后只是对比新旧两条链,各两个节点的问题

声明四个指针,指向新旧两条链的两端。

每次循环,都会两两对比,这四个指针所指向的节点。

如果指向头部的指针匹配到了对应的节点,则头部指针向尾部移动一个位置

如果指向尾部的指针匹配到了对应的节点,则尾部指针向头部移动一个位置

结束条件:两条链都头 指针的索引 > 尾 指针的索引 了

会依次对比这四个:

新头和旧头

无需移动

如果匹配上,更新 dom 节点。

新尾和旧尾

无需移动

如果匹配上,更新 dom 节点

新尾和旧头

如果匹配上,需要移动

把旧头移动到旧尾的后面

新头和旧尾

如果匹配上,需要移动

把旧尾移动到旧头的前面

如果以上4个条件都没满足

拿新头去旧链中去找key相同的节点

  • 如果找到
    1. 把旧链中找到的节点对应的dom移动到,旧头之前
    2. 把旧链中这个索引处置为undefined
    3. 以后循环的时候,如果旧头是undefined,则直接进行下一次循环
  • 如果没找到
    • 代表这是一个新节点。直接挂载到旧头之前

如果循环结束后还有节点遗漏

  • 新链中还有节点未被处理。则挂载这些节点

  • 旧链中还有节点未被处理,则卸载这些节点

快速diff

处理相同的前置和后置元素

先从两端遍历新旧两条链,把两端位置没有变的节点都找出来,更新他们。

然后进入下一步,处理两条链剩余的部分的移动,挂载和卸载

移动,挂载和卸载

  1. 把新链中的剩余节点在旧链中的索引,保存成一个数组:source。默认值为-1,代表是新节点需要被挂载。

  2. 求这个数组的最长递增子序

    source数组中每一项代表的节点,在新链中的索引的递增的

    最长递增子序中每一项代表的节点,在旧链中也是递增的。

    那么:,最长递增子序列中每一项代表的节点的顺序,在更新前后没有变化

    那么:只需要移动其他节点的位置即可

  3. 遍历新链,找到所有不在最长递增子序列中的节点,把这些节点移动到在新链中对应的索引位置处即可

    把i节点,移动或创建到i+1节点之前。如果i+1节点不存在,就放到父容器尾部。

相关推荐
Fan_web9 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常10 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记4 小时前
【复习】HTML常用标签<table>
前端·html
程序员大金4 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js