vue diff 前后缀+最长递增子序列算法

news2024/11/24 14:29:44

文章目录

  • 查找相同前后缀
    • 通过前后缀位置信息新增节点
    • 通过前后缀位置信息删除节点
  • 中间部份 diff
    • 判断节点是否需要移动
    • 删除节点
      • 删除未查找到的节点
      • 删除多余节点
    • 移动和新增节点
      • 最长递增子序列
    • 求解最长递增子序列位置信息

查找相同前后缀

在这里插入图片描述

  • 如上图所示,新旧 children 拥有相同的前缀节点和后缀节点
  • 对于前缀节点,我们可以建立一个索引,指向新旧 children 中的第一个节点,并逐步向后遍历,直到遇到两个拥有不同 key 值的节点为止
// 更新相同的前缀节点
// j 为指向新旧 children 中第一个节点的索引
let j = 0
let prevVNode = prevChildren[j]
let nextVNode = nextChildren[j]
// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
while (prevVNode.key === nextVNode.key) {
  // 调用 patch 函数更新
  patch(prevVNode, nextVNode, container)
  j++
  prevVNode = prevChildren[j]
  nextVNode = nextChildren[j]
}

  • 对于相同的后缀节点,由于新旧 children 中节点的数量可能不同,所以我们需要两个索引分别指向新旧 children 的最后一个节点,并逐步向前遍历,直到遇到两个拥有不同 key 值的节点为止
// 更新相同的后缀节点

// 指向旧 children 最后一个节点的索引
let prevEnd = prevChildren.length - 1
// 指向新 children 最后一个节点的索引
let nextEnd = nextChildren.length - 1

prevVNode = prevChildren[prevEnd]
nextVNode = nextChildren[nextEnd]

// while 循环向前遍历,直到遇到拥有不同 key 值的节点为止
while (prevVNode.key === nextVNode.key) {
  // 调用 patch 函数更新
  patch(prevVNode, nextVNode, container)
  prevEnd--
  nextEnd--
  prevVNode = prevChildren[prevEnd]
  nextVNode = nextChildren[nextEnd]
}

通过前后缀位置信息新增节点

// 前缀节点终止位置
j: 1
// 后缀节点终止位置
prevEnd: 0
nextEnd: 1

  • 发现 j > prevEnd 并且 j <= nextEnd,这说明当新旧 children 中相同的前缀和后缀被更新之后,旧 children 中的节点已经被更新完毕了,而新 children 中仍然有剩余节点,通过上图可以发现,新 children 中的 li-d 节点,就是这个剩余的节点。实际上新 children 中位于 j 到 nextEnd 之间的所有节点都应该是新插入的节点:
    在这里插入图片描述
// 满足条件,则说明从 j -> nextEnd 之间的节点应作为新节点插入
if (j > prevEnd && j <= nextEnd) {
  // 所有新节点应该插入到位于 nextPos 位置的节点的前面
  const nextPos = nextEnd + 1
  const refNode =
    nextPos < nextChildren.length ? nextChildren[nextPos].el : null
  // 采用 while 循环,调用 mount 函数挂载节点
  while (j <= nextEnd) {
    mount(nextChildren[j++], container, false, refNode)
  }
}

通过前后缀位置信息删除节点

在这里插入图片描述

  • 在这个案例中,当“去掉”相同的前缀和后缀之后,三个索引的值为:
j: 1
prevEnd: 1
nextEnd: 0
  • 这时条件 j > nextEnd 并且 j <= prevEnd 成立,通过上图可以很容的发现,旧 children 中的 li-b 节点应该被移除,实际上更加通用的规则应该是:在旧 children 中有位于索引 j 到 prevEnd 之间的节点,都应该被移除
if (j > prevEnd && j <= nextEnd) {
  // j -> nextEnd 之间的节点应该被添加
  const nextPos = nextEnd + 1
  const refNode =
    nextPos < nextChildren.length ? nextChildren[nextPos].el : null
  while (j <= nextEnd) {
    mount(nextChildren[j++], container, false, refNode)
  }
} else if (j > nextEnd) {
  // j -> prevEnd 之间的节点应该被移除
  while (j <= prevEnd) {
    container.removeChild(prevChildren[j++].el)
  }
}

  • 假设在第一个 while 循环结束之后,索引 j 的值已经大于 prevEnd 或 nextEnd,那么还有必须执行第二个 while 循环吗?答案是没有必要,这是因为一旦索引 j 大于 prevEnd 则说明旧 children 中的所有节点都已经参与了 patch,类似的,如果索引 j 大于 nextEnd 则说明新 children 中的所有节点都已经参与了 patch,这时当然没有必要再执行后续的操作了。
while (prevVNode.key === nextVNode.key) {
  patch(prevVNode, nextVNode, container)
  j++
  if (j > prevEnd || j > nextEnd) {
    break;
  }
  prevVNode = prevChildren[j]
  nextVNode = nextChildren[j]
}
// 更新相同的后缀节点
prevVNode = prevChildren[prevEnd]
nextVNode = nextChildren[nextEnd]
while (prevVNode.key === nextVNode.key) {
  patch(prevVNode, nextVNode, container)
  prevEnd--
  nextEnd--
  if (j > prevEnd || j > nextEnd) {
    break outer
  }
  prevVNode = prevChildren[prevEnd]
  nextVNode = nextChildren[nextEnd]
}

if(!(j > prevEnd && j>prevEnd)){
	// 满足条件,则说明从 j -> nextEnd 之间的节点应作为新节点插入
	if (j > prevEnd && j <= nextEnd) {
	  // j -> nextEnd 之间的节点应该被添加
	  // 省略...
	} else if (j > nextEnd) {
	  // j -> prevEnd 之间的节点应该被移除
	  // 省略...
	}
}
 

中间部份 diff

在这里插入图片描述

  • 首先,我们需要构造一个数组 source,该数组的长度等于新 children 在经过预处理之后剩余未处理节点的数量,初始化该数组中每个元素的初始值为 -1
  • 实际上 source 数组将用来存储新 children 中的节点在旧 children 中的位置,后面将会使用它计算出一个最长递增子序列,并用于 DOM 移动
    在这里插入图片描述
  • 再建立一个 Index Map 中的键是节点的 key,值是节点在新 children 中的位置索引,用空间来换取时间上的优化
    在这里插入图片描述
if (j > prevEnd && j <= nextEnd) {
  // 省略...
} else if (j > nextEnd) {
  // 省略...
} else {
	// 构造 source 数组
	const nextLeft = nextEnd - j + 1  // 新 children 中剩余未处理节点的数量
	const source = []
	for (let i = 0; i < nextLeft; i++) {
	  source.push(-1)
	}
	
	const prevStart = j
	const nextStart = j
	let moved = false
	let pos = 0
	// 构建索引表
	const keyIndex = {}
	for(let i = nextStart; i <= nextEnd; i++) {
	  keyIndex[nextChildren[i].key] = i
	}
	// 遍历旧 children 的剩余未处理节点
	for(let i = prevStart; i <= prevEnd; i++) {
	  prevVNode = prevChildren[i]
	  // 通过索引表快速找到新 children 中具有相同 key 的节点的位置
	  const k = keyIndex[prevVNode.key]
	
	  if (typeof k !== 'undefined') {
	    nextVNode = nextChildren[k]
	    // patch 更新
	    patch(prevVNode, nextVNode, container)
	    // 更新 source 数组
	    source[k - nextStart] = i
	    // 判断是否需要移动
	    if (k < pos) {
	      moved = true
	    } else {
	      pos = k
	    }
	  } else {
	    // 没找到
	  }
	}

}


判断节点是否需要移动

  • 在上一步代码中,遍历旧 children 的剩余未处理节点,通过索引表快速找到新 children 中具有相同 key 的节点的位置
  • 如果新旧节点位置没有变化,那么位置信息应该是相同的,否则,新节点索引表信息为[1,2,3,4],如果映射成旧节点为[1,2,4,3],说明旧节点的最后一个位置和前面的位置互换了,说明节点移动了
const prevStart = j
const nextStart = j
let moved = false
let pos = 0
// 构建索引表
const keyIndex = {}
for(let i = nextStart; i <= nextEnd; i++) {
  keyIndex[nextChildren[i].key] = i
}
// 遍历旧 children 的剩余未处理节点
for(let i = prevStart; i <= prevEnd; i++) {
  prevVNode = prevChildren[i]
  // 通过索引表快速找到新 children 中具有相同 key 的节点的位置
  const k = keyIndex[prevVNode.key]

  if (typeof k !== 'undefined') {
    nextVNode = nextChildren[k]
    // patch 更新
    patch(prevVNode, nextVNode, container)
    // 更新 source 数组
    source[k - nextStart] = i
    // 判断是否需要移动
    if (k < pos) {
      moved = true
    } else {
      pos = k
    }
  } else {
    // 没找到
  }
}

删除节点

删除未查找到的节点

  • 拿旧 children 中的节点尝试去新 children 中寻找具有相同 key 值的节点,但并非总是能够找得到,当 k === ‘undefined’ 时,说明该节点在新 children 中已经不存在了,这时我们应该将其移除
// 遍历旧 children 的剩余未处理节点
for(let i = prevStart; i <= prevEnd; i++) {
  prevVNode = prevChildren[i]
  // 通过索引表快速找到新 children 中具有相同 key 的节点的位置
  const k = keyIndex[prevVNode.key]

  if (typeof k !== 'undefined') {
    // 省略...
  } else {
    // 没找到,说明旧节点在新 children 中已经不存在了,应该移除
    container.removeChild(prevVNode.el)
  }
}

删除多余节点

  • 旧 children 中已经更新过的节点数量应该小于新 children 中需要更新的节点数量,一旦更新过的节点数量超过了新 children 中需要更新的节点数量,则说明该节点是多余的节点,我们也应该将其移除
const nextLeft = nextEnd - j + 1  // 新 children 中剩余未处理节点的数量
let patched = 0
// 遍历旧 children 的剩余未处理节点
for (let i = prevStart; i <= prevEnd; i++) {
  prevVNode = prevChildren[i]

  if (patched < nextLeft) {
    // 通过索引表快速找到新 children 中具有相同 key 的节点的位置
    const k = keyIndex[prevVNode.key]
    if (typeof k !== 'undefined') {
      nextVNode = nextChildren[k]
      // patch 更新
      patch(prevVNode, nextVNode, container)
      patched++
      // 更新 source 数组
      source[k - nextStart] = i
      // 判断是否需要移动
      if (k < pos) {
        moved = true
      } else {
        pos = k
      }
    } else {
      // 没找到,说明旧节点在新 children 中已经不存在了,应该移除
      container.removeChild(prevVNode.el)
    }
  } else {
    // 多余的节点,应该移除
    container.removeChild(prevVNode.el)
  }
}

移动和新增节点

在这里插入图片描述

最长递增子序列

  • source 数组的值为 [2, 3, 1, -1],很显然最长递增子序列应该是 [ 2, 3 ],换算成位置信息是 [ 0, 1 ]
  • 而最长递增子序列是 [ 0, 1 ] 这告诉我们:新 children 的剩余未处理节点中,位于位置 0 和位置 1 的节点的先后关系与他们在旧 children 中的先后关系相同。或者我们可以理解为位于位置 0 和位置 1 的节点是不需要被移动的节点
  • 只有 li-b 节点和 li-g 节点是可能被移动的节点,但是我们发现与 li-g 节点位置对应的 source 数组元素的值为 -1,这说明 li-g 节点应该作为全新的节点被挂载,所以只有 li-b 节点需要被移动
  • 使用 for 循环从后向前遍历新 children 中剩余未处理的子节点
  • 这里的技巧在于 i 的值的范围是 0 到 nextLeft - 1,这实际上就等价于我们对剩余节点进行了重新编号。接着判断当前节点的位置索引值 i 是否与子序列中位于 j 位置的值相等,如果不相等,则说明该节点需要被移动;如果相等则说明该节点不需要被移动,并且会让 j 指向下一个位置
  • 节点需要被怎么移动呢?找到 li-b 节点的后一个节点(li-g),将其插入到 li-g 节点的前面即可
if (moved || source.indexOf(-1)!==-1) {
  // 根据 source 数组计算一个最长递增子序列:
  const seq = lis(sources) // [ 0, 1 ],代表的是最长递增子序列中的各个元素在 source 数组中的位置索引
  let j = seq.length - 1
  // 从后向前遍历新 children 中的剩余未处理节点
  for (let i = nextLeft - 1; i >= 0; i--) {
    if (source[i] === -1) {
      // 作为全新的节点挂载

      // 该节点在新 children 中的真实位置索引
      const pos = i + nextStart
      const nextVNode = nextChildren[pos]
      // 该节点下一个节点的位置索引
      const nextPos = pos + 1
      // 挂载
      mount(
        nextVNode,
        container,
        false,
        nextPos < nextChildren.length
          ? nextChildren[nextPos].el
          : null
      )
    } else if (i !== seq[j]) {
      // 说明该节点需要移动
      // 该节点在新 children 中的真实位置索引
      const pos = i + nextStart
      const nextVNode = nextChildren[pos]
      // 该节点下一个节点的位置索引
      const nextPos = pos + 1
      // 移动
      container.insertBefore(
        nextVNode.el,
        nextPos < nextChildren.length
          ? nextChildren[nextPos].el
          : null
      )
    } else {
      // 当 i === seq[j] 时,说明该位置的节点不需要移动
      // 并让 j 指向下一个位置
      j--
    }
  }
}

}

求解最长递增子序列位置信息

[ 0, 8, 4, 12, 2, 10]
// 最长递增子序列长度
[ 3, 2, 2, 1, 2, 1]
  • 如上可知最长子序列长度为 3,从右往左遍历查找子序列长度为2位置,接着查找为1的位置,这样就能找到所有的位置信息
// 所有的最长递增子序列
[ 0, 8, 12 ]
[ 0, 8, 10 ]
[ 0, 4, 12 ]
[ 0, 4, 10 ]
[ 0, 2, 10 ]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/841584.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SCT82A30DHKR_5.5V-100V Vin同步降压控制器

SCT82A30是一款100V电压模式控制同步降压控制器&#xff0c;具有线路前馈。40ns受控高压侧MOSFET的最小导通时间支持高转换比&#xff0c;实现从48V输入到低压轨的直接降压转换&#xff0c;降低了系统复杂性和解决方案成本。如果需要&#xff0c;在低至6V的输入电压下降期间&am…

Photoshop 2023 25.0beta「Mac」

Photoshop 2023是一款专业图像处理软件&#xff0c;它主要用于图像编辑、合成和设计等方面。 Photoshop beta创新式填充的功能特色包括&#xff1a; 自动识别和删除对象&#xff1a;该功能可以自动识别图像中的对象&#xff0c;并用周围的图像填充空白部分&#xff0c;使图像看…

window系统下 tinymce富文本编辑器在搜狗输入法下placeholder不消失现象

window 搜狗输入法下编辑器占位符和内容重叠问题 这种情况是&#xff0c;tinymce插件库存在一些兼容BUG&#xff0c;需要我们自行手写样式或者js替换掉placeholder&#xff0c;代码如下&#xff1a; // 获取富文本框的内容const handleChange (editorContent) > {// cons…

C++11 新特性 ---- final 和 override

一、final C中增加了final关键字&#xff0c;作用如下&#xff1a; ① 限制某个类不能被继承② 或者某个虚函数不能被重写 ① 限制某个类不能被继承 // ① 限制某个类不能被继承,也就是说这个类不能有派生类 class Base{ public:virtual void print() {cout<<"Ba…

电商数据获取:网络爬虫还是付费数据接口?

随着电商行业的迅速发展&#xff0c;对电商数据的需求也越来越大。在获取电商数据时&#xff0c;常常面临一个选择&#xff1a;是自己编写网络爬虫进行数据爬取&#xff0c;还是使用现有的付费数据接口呢&#xff1f;本文将从成本、可靠性、数据质量等多个角度进行分析&#xf…

【css】组合器

组合器是解释选择器之间关系的某种机制。在简单选择器器之间&#xff0c;可以包含一个组合器&#xff0c;从而实现简单选择器难以达到的效果。 CSS 中有四种组合器&#xff1a; 后代选择器 (空格)&#xff1a;匹配属于指定元素后代的所有元素&#xff0c;示例&#xff1a;div …

element-ui表格数据为空,图片占位提示

当表格的绑定数据为空时常需要显示暂无数据等字样&#xff0c;这时候就用到了empty-text <el-table:data"tableData"stripeborderempty-text"暂无数据"> 但&#xff0c;当数据为空&#xff0c;想用图片展示呢&#xff0c;如下图 方法一&#xff1a…

java.lang.UnsupportedClassVersionError TestCase

JavaFramework-JDK6.jar 放到JDK17运行没有问题 JavaFramework源码放到JDK17环境下编译出来的JavaFramework-JDK17.jar JavaFramework-JDK17.jar 放到JDK17运行没有问题 JavaFramework-JDK17.jar 放到JDK8运行没有问题&#xff0c;这个好像不对啊&#xff0c;可能之前编译设置…

day39反转字符串总结

反转字符串原理其实就是交换位置&#xff0c;以中间为分隔点&#xff1b; 基本套路&#xff1a;遍历前一般字符&#xff0c;互换位置&#xff1b; for循环模板 void reverseString(char* s, int sSize){char temp;for (int i 0, j sSize - 1; i < sSize/2; i, j--) {temp…

【无公网IP】本地电脑搭建个人博客网站(并发布公网访问 )和web服务器

【无公网IP】本地电脑搭建个人博客网站&#xff08;并发布公网访问 &#xff09;和web服务器 文章目录 【无公网IP】本地电脑搭建个人博客网站&#xff08;并发布公网访问 &#xff09;和web服务器前言1. 安装套件软件2. 创建网页运行环境 指定网页输出的端口号3. 让WordPress在…

【Rust】Rust学习第三章常见编程概念

包含第一、二章 文档&#xff1a;Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (bootcss.com) 墙裂推荐这个文档 第一章入门 入门指南 - Rust 程序设计语言 简体中文版 第二章猜猜看游戏 猜猜看游戏教程 - Rust 程序设计语言 简体中文版 (bootcss.com) // 导入库 us…

Stable Diffusion 硬核生存指南:WebUI 中的 GFPGAN

本篇文章聊聊 Stable Diffusion WebUI 中的核心组件&#xff0c;强壮的人脸图像面部画面修复模型 GFPGAN 相关的事情。 写在前面 本篇文章的主角是开源项目 TencentARC/GFPGAN&#xff0c;和上一篇文章《Stable Diffusion 硬核生存指南&#xff1a;WebUI 中的 CodeFormer》提…

H263压缩码流如何分解为一个一个单元并查询到其宽高?

H263码流尺寸规格有限&#xff0c;只有以下几种&#xff1a; H263码流有四个分层&#xff1a; 1、图像层 2、块组 3、宏块 4、块 下面分别介绍&#xff1a; 具体介绍如下&#xff0c;5.1.3中红色框选部分就是压缩码流的宽高指示&#xff1a; 图像层 上面就是H263的图像层&am…

QT开发学习相关笔记

QT中配置文件读取 QT中使用的config文件为&#xff1a;xxx.ini文件,基本格式如下&#xff1a; 使用 QSetting&#xff08;QT自带类&#xff09;中的相关接口实现设置配置文件数据&#xff0c;或者读取数据。 读取配置文件路径设置如下&#xff0c;其中 iniPath为设置路径 ne…

word2003脚注问题

问题分析&#xff1a; 在题目上插入脚注的时候&#xff0c;脚注放在文件结尾&#xff0c;然后正文拆开了&#xff0c;不能续前节 解决办法&#xff1a; word2003中&#xff0c;工具->选项->兼容性

进程间通讯(IPC机制) 管道 信号量 共性内存 消息队列 详细图解

进程间通讯-IPC机制 常用命令管道有名管道读写编程有名管道示意图 无名管道 信号量信号量的概念信号量接口函数进程 a 和进程 b 模拟访问打印机 用信号量互斥画图分析代码实现测试结果显示和操作 共享内存 信号量 消息队列 的命令 共享内存共享内存定义共享内存函数接口实例编程…

docker镜像push到仓库

镜像可以很方便直接 push 到 docker 的公共仓库或阿里云仓库 一、Dockerpush指定仓库是什么&#xff1f; Dockerpush是Docker的一个命令&#xff0c;用于将本地的Docker镜像推送到Docker官方公共仓库或用户私人仓库。而指定仓库则是将这个Docker镜像推送到指定的仓库中。 通过D…

【独立后台】快递小程序便宜寄快递系统小程序 对接易达

快递代发项目简介&#xff1a; 顾名思义就是帮发快递。原本产业链是客户-快递之间的联系&#xff0c;现在变成了客户-我们-快递&#xff0c;简单来说就是我们把客户聚集到一起团购到了更优惠的价格。很简单就是赚一个差价&#xff0c; 单子多就能和各个快递合作的平台&#x…

C++ 类型兼容规则

类型兼容规则是指在需要基类对象的任何地方&#xff0c;都可以使用公有派生类的对象来替代。 通过公有继承&#xff0c;派生类得到了基类中除构造函数和析构函数之外的所有成员。这样&#xff0c;公有派生类实际就具备了基类的所有功能&#xff0c;凡是基类能解决的问题&#x…

QA | 关于手持式频谱仪,您想了解的那些技术问题(二)

Q1&#xff1a;手持式频谱仪的灵敏度多高&#xff1f;底噪多少&#xff1f; 0.01-3GHz手持频谱仪的底噪/灵敏度为-128dBm RBW10kHz&#xff08;即归一化到Hz为-168dBm/Hz&#xff09;&#xff1b;2-8GHz手持频谱仪的底噪/灵敏度为-119dBm RBW30kHz&#xff08;即归一化到Hz为…