Vue2 Diff 算法简易版

news2024/11/28 8:40:32

背景

最近复习的过程中,准备对比一下Vue2和Vue3的diff算法区别,好知道两者直接的差异和优缺点。刚好看了网上的文章,但是对方写的代码不太正确,所以特意记录一下我的学习过程~

双端比较法

Vue2采用的双端比较法,即新列表和旧列表进行头尾对比,在对比的过程中指针会逐渐向内靠拢,直到某个列表的节点全部遍历过,对比停止。
举个例子:

# 旧列表
a b c d
# 新列表
d b a c

我们会先定义几个变量:

function vue2diff(prevChildren, nextChildren, parent) {
	let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
}

那么此时我们就有四个指针和四个指针对应的节点了,分别是旧列表的节点a、旧列表的节点d、新列表的节点d和新列表的节点c,为了下面方便表述:旧列表的节点a会写为oldNodeA,以此类推~
接下来说一下双端比较法的比较方法:

  1. oldStartNodenewStartNode对比key值;
  2. oldEndNode newEndNode 对比key值;
  3. oldStartNodenewEndNode 对比key值;
  4. newStartNodeoldEndNode 对比key值。
    如图所示:
    在这里插入图片描述

对比流程

接下来进入我们的对比流程~对比的过程主要是寻找拥有相同key值的节点

实现双端对比

先说一下双端对比中,如果遇到相同节点(拥有通过key)的情况:

  • oldStartNodenewStartNodekey相同时,则oldStartIndexnewStartIndex同时向后移动一位;
  • oldEndNodenewEndNodekey相同时,则oldEndIndexnewEndIndex同时向前移动一位;
  • oldStartNodenewEndNodekey相同时,则oldStartIndex向后移动一位,newEndIndex向前移动一位;
  • oldEndNodenewStartNodekey相同时,则oldEndIndex向前移动一位,newStartIndex向后移动一位。

结束循环的条件当其中一个列表的节点全部遍历完成时,则完成我们的对比过程。

按这个为例子:

# 旧列表
a b c d
# 新列表
d b a c

第一次对比的时:
在这里插入图片描述

  • oldStartNode A newStartNode D不同,继续对比;
  • oldEndNode DnewEndNode C不同,继续对比;
  • oldEndNode DnewStartNode D相同,则这时候会将旧列表中得到节点D的位置进行变化,挪到A的前面,然后oldEndIndex向前移动,newStartIndex向后移动。
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d a b c

接下来继续对比:

  • oldStartNode A newStartNode B不同,继续对比;
  • -oldEndNode CnewEndNode C相同,但是它们都属于尾节点,所以我们直接复用节点,将oldEndIndexnewEndIndex都向前移动
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d a b c

接下来继续对比:

  • oldStartNode A newStartNode B不同,继续对比;
  • oldEndNode B newEndNode A不同,继续对比;
  • oldStartNode A newEndNode A,则将旧列表中A节点挪到B节点后面即可,oldStartIndex向后移动一位,newEndIndex向前移动一位
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d b a c

最后就是oldEndNode B newEndNode B对比,一致,然后结束了循环~
大致代码如下:

function vue2diff(prevChildren, nextChildren, parent) {
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    if (oldStartNode.key === newStartNode.key) {
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
      // ...
    }
  }
}

以上是四个头尾新旧节点对比会出现相同节点的情况,接下来我们来看一下四次对比找不到复用节点的情况:
在这里插入图片描述

  • oldStartNode AnewStartNode E 不同;
  • oldEndNode DnewEndNode H 不同;
  • oldStartNode AnewEndNode H 不同;
  • oldEndNode D 与 newStartNode E 不同;

这时候我们会先拿出新列表的第一个节点oldStartNode A,然后找到旧列表中是否存在可以复用的节点,这里就有两种情况啦,先说图上的这种:如果不存在,那么说明这个是新节点,直接放在旧列表的最前面即可;
如果是这种:
在这里插入图片描述
B的复用节点在旧列表中,那么将旧节点移动到第一个节点,并在旧列表中置成undefine
在这里插入图片描述

function vue2diff(prevChildren, nextChildren, parent) {
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    if (oldStartNode.key === newStartNode.key) {
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
        mount(newStartNode, parent, oldStartNode.el)
      } else {
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
}

这时候需要考虑的问题是,当进入下一个循环的时候,对应的oldStartNode为undefined,所以代码上需要处理一下:

function vue2diff(prevChildren, nextChildren, parent) {
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
      // ...
    } else if (oldEndNode.key === newEndNode.key) {
      // ...
    } else if (oldStartNode.key === newEndNode.key) {
      // ...
    } else if (oldEndNode.key === newStartNode.key) {
      // ...
    } else {
      // ...
    }
  }
  
}

接下来我们来考虑一下列表删除的情况:
在这里插入图片描述

  • 节点A复用;
  • 节点D复用;
    此时各个坐标变化如下:
    在这里插入图片描述
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    while (oldStartIndex <= oldStartIndex) {
      if (!prevChildren[oldStartIndex]) {
        oldStartIndex++
        continue
      }
      // 删除节点
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } 

最后是新增节点的情况啦~
在这里插入图片描述

  • 节点A复用;
  • 节点B复用;
    此时各个坐标变化如下:
    在这里插入图片描述
if (oldStartIndex> oldEndIndex ) {
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }

最后贴一下完整代码:

function vue2diff(prevChildren, nextChildren, parent) {
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
        mount(newStartNode, parent, oldStartNode.el)
      } else {
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
  // 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    while (oldStartIndex <= oldStartIndex) {
      if (!prevChildren[oldStartIndex]) {
        oldStartIndex++
        continue
      }
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } else if (oldStartIndex> oldEndIndex ) {
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }
}

小结

重新学了一遍Vue2的diff算法:双端比较法,有了新的收获吧,思路清晰了很多。后面会写一篇Vue3的diff算法~

参考链接

  • 【Vue源码】第十八节DOM更新以及diff算法
  • React、Vue2、Vue3的三种Diff算法

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

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

相关文章

MBD开发 STM32 Timer

开两个定时器 一快一慢 两个中断都要使能 没有自动更新&#xff0c;切换下timerx就好了&#xff0c;但是触发UP要手动勾选

剑指offer27.二叉树的镜像

这道题很简单&#xff0c;写了十多分钟就写出来了&#xff0c;一看题目就知道这道题肯定要用递归。先交换左孩子和右孩子&#xff0c;再用递归交换左孩子的左孩子和右孩子&#xff0c;交换右孩子的左孩子和右孩子&#xff0c;其中做一下空判断就行。以下是我的代码&#xff1a;…

爬虫入门指南(8): 编写天气数据爬虫程序,实现可视化分析

文章目录 前言准备工作爬取天气数据可视化分析完整代码解释说明 运行效果完结 前言 天气变化是生活中一个重要的因素&#xff0c;了解天气状况可以帮助我们合理安排活动和做出决策。本文介绍了如何使用Python编写一个简单的天气数据爬虫程序&#xff0c;通过爬取指定网站上的天…

Pandas+Pyecharts | 双十一美妆销售数据分析可视化

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 数据信息2.3 筛选有销量的数据 &#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 双十一前后几天美妆订单数量3.2 双十一前后几天美妆销量3.3…

【Linux】线程终结篇:线程池以及线程池的实现

linux线程完结 文章目录 前言一、线程池的实现二、了解性知识 1.其他常见的各种锁2.读者写者问题总结 前言 什么是线程池呢&#xff1f; 线程池一种线程使用模式。线程过多会带来调度开销&#xff0c;进而影响缓存局部性和整体性能。而线程池维护着多个线程&#xff0c;等待着…

智能、安全、高效,看移远如何助力割草机智能化升级

提到割草机&#xff0c;大家可能首先会想到其噪声大、费人力、安全性不足等问题。智能割草机作为一种便捷、高效的智能割草设备&#xff0c;能够自主完成草坪修剪工作&#xff0c;很好地解决传统割草机的痛点问题。 随着人们对家庭园艺以及生活质量要求的逐步提高&#xff0c;割…

向量数据库:新一代的数据处理工具

在我们的日常生活中&#xff0c;数据无处不在。从社交媒体的帖子到在线购物的交易记录&#xff0c;我们每天都在产生和处理大量的数据。为了有效地管理这些数据&#xff0c;我们需要使用数据库。数据库是存储和管理数据的工具&#xff0c;它们可以按照不同的方式组织和处理数据…

python实现简单贪吃蛇

import math import pygame import time import numpy as np # 此模块包含游戏所需的常量 from pygame.locals import *# 设置棋盘的长宽 BOARDWIDTH 90 BOARDHEIGHT 50 # 分数 score 0# 豆子 class Food(object):def __init__(self):self.item (4, 5)# 画出食物def _draw(…

qtav源码包编译(qt5.15+msvc2019)、使用vlc media player串流生成rtsp的url并且在qml客户端中通过qtav打开

QTAV源码包编译 下载源码 下载依赖库&#xff08;里面有ffmepg等内容&#xff09; https://sourceforge.net/projects/qtav/files/depends/QtAV-depends-windows-x86x64.7z/download下载源码包 https://github.com/wang-bin/QtAV更新子模块 cd QtAV && git submod…

vmware postgresql大杂烩

Vmware 窗口过界&#xff1a; https://blog.csdn.net/u014139753/article/details/111603882 vmware, ubuntu 安装&#xff1a; https://zhuanlan.zhihu.com/p/141033713 https://blog.csdn.net/weixin_41805734/article/details/120698714 centos安装&#xff1a; https://w…

【Go】短信内链接拉起小程序

一、 需求场景 (1) 业务方&#xff0c;要求给用户发送的短信内含有可以拉起我们的小程序指定位置的链接&#xff1b; 【XXX】尊敬的客户&#xff0c;您好&#xff0c;由于您XX&#xff0c;请微信XX小程序-微信授权登录-个人中心去XX&#xff0c;如已操作请忽略&#xff0c;[…

Jenkins2.346新建项目时没有Maven项目选项解决办法

解决办法&#xff1a;需要安装Maven Integration 系统管理-->管理插件-->可选插件-->过滤输入框中输入搜索关键字&#xff1a; Maven Integration&#xff0c;下载好后安装。

Mysql:创建和管理表(全面详解)

创建和管理表 前言一、基础知识1、一条数据存储的过程2、标识符命名规则3、MySQL中的数据类型 二、创建和管理数据库1、创建数据库2、使用数据库3、修改数据库4、删除数据库 三、创建表1、创建方式12、创建方式23、查看数据表结构 四、修改表1、追加一个列2、修改一个列3、重命…

MySQL存储引擎(InnoDB、MyISAM、Memory面试题)

1.1 MySQL体系结构 1). 连接层 最上层是一些客户端和链接服务&#xff0c;包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念&#xff0c;为通过认证安全接入的…

暑期学JavaScript【第六天】

一、正则表达式 边界符 ^&#xff1a;表示以后面字符开头 $&#xff1a;表示以前方字符结尾量词 *:前面的字符至少出现0次 :前面的字符至少出现1次 ?:前面的字符出现0/1次 {n}:重复n次 {n,}:至少重复n次 {n,m}:重复n~m次字符类 [ ]:代表字符集合 /^[a-z]$/[ ^ ] 取反 [^a-…

leetcode 257. 二叉树的所有路径

2023.7.5 这题需要用到递归回溯&#xff0c;也是我第一次接触回溯这个概念。 大致思路是&#xff1a; 在reversal函数中&#xff0c;首先将当前节点的值加入到路径path中。然后判断当前节点是否为叶子节点&#xff0c;即没有左右子节点。如果是叶子节点&#xff0c;将路径转化…

nnUNet保姆级使用教程!从环境配置到训练与推理(新手必看)

文章目录 写在前面nnUNet是什么&#xff1f;一、配置虚拟环境二、安装nnUNet框架1.安装nnUNet这一步我遇到的两个问题&#xff1a; 2.安装隐藏层hiddenlayer&#xff08;可选&#xff09; 三、数据集准备nnUNet对于你要训练的数据是有严格要求的&#xff0c;这第一点就体现在我…

apple pencil值不值得购买?ipad可以用的手写笔推荐

现在市面的电容笔品牌鱼龙混杂&#xff0c;我们很在选购中很容易就踩坑&#xff0c;例如买到一些书写会频繁出现断触的&#xff0c;或者防误触功能会失灵。所以我们在选购中务必要擦亮双眼。而对于一些将ipad作为一种学习工具的人而言&#xff0c;电容笔已经是iPad中不可或缺的…

【C++】vector基本用法介绍

vector简单介绍 前言vector原型vector常用函数接口介绍vector的构造、析构、赋值构造析构 修改类的函数push_backinsertfind 函数 eraseswap 关于容量的函数max_sizesort vector\<char\> 和 string的区别vector\<数据类型\> 结束 前言 首先&#xff0c;vector的用…

vue watch

Vue.js已在全球开发人员中广受欢迎&#xff0c;这归功于其灵活的响应式系统和丰富的开发工具。本文将深入解析Vue中的Watch特性&#xff0c;我们将了解其功能&#xff0c;适用的实际例子&#xff0c;以及可能遇到的常见错误及其解决方案。 第一部分&#xff1a;Vue的Watch特性…