【手写 Vue2.x 源码】第三十一篇 - diff算法-比对优化(下)

news2024/11/18 9:28:30

一,前言

上篇,diff算法-比对优化(上),主要涉及以下几个点:

  • 介绍了如何进行儿子节点比对;
  • 新老儿子节点可能存在的3种情况及代码实现;
  • 新老节点都有儿子时的 diff 方案介绍与处理逻辑分析;

本篇,diff算法-比对优化(下)


二,比对优化

1,前文回顾

上篇介绍了新老儿子节点可能存在的几种情况及处理方法

  • 情况 1:老的有儿子,新的没有儿子

    处理方法:直接将多余的老 dom 元素删除即可;

  • 情况 2:老的没有儿子,新的有儿子

    处理方法:直接将新的儿子节点放入对应的老节点中即可;

  • 情况 3:新老都有儿子

    处理方法:进行 diff 比对;

针对情况 3 新老儿子节点的比对,采用了头尾双指针的方法:

image.png

优先对新老儿子的头头、尾尾、头尾、尾头节点进行比对,若都没有命中再进行乱序比对

2,节点比对的结束条件

直至新老节点一方遍历完成,比对才结束;

即:“老的头指针和尾指针重合"或"新的头指针和尾指针重合”;

image.png

此时,就是循环中最后一次比对了,D 节点比对完成后节点继续后移

与老节点比对完成后(已经识别了可复用的节点),继续将新增节点 E 添加到老儿子节点中

代码实现:

// src/vdom/patch.js

/**
 * 新老都有儿子时做比对,即 diff 算法核心逻辑
 * 备注:采用头尾双指针的方式;优化头头、尾尾、头尾、尾头的特殊情况;
 * @param {*} el 
 * @param {*} oldChildren  老的儿子节点
 * @param {*} newChildren  新的儿子节点
 */
function updateChildren(el, oldChildren, newChildren) {
    
    // 声明头尾指针
    let oldStartIndex = 0;
    let oldStartVnode = oldChildren[0];
    let oldEndIndex = oldChildren.length - 1;
    let oldEndVnode = oldChildren[oldEndIndex];

    let newStartIndex = 0;
    let newStartVnode = newChildren[0];
    let newEndIndex = newChildren.length - 1;
    let newEndVnode = newChildren[newEndIndex];

    // 循环结束条件:有一方遍历完了就结束;即"老的头指针和尾指针重合"或"新的头指针和尾指针重合" 
    while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
        // 1,优先做4种特殊情况比对:头头、尾尾、头尾、尾头
        // 2,如果没有命中,采用乱序比对
        // 3,比对完成后移动指针,继续下一轮比对
    }

    // 比对完成后
    // 新的多,插入新增节点,删除多余节点
}

备注:由于diff 算法采用了 while 循环处理,所以复杂度为O(n)

3,情况 1:新儿子比老儿子多,插入新增的

分为“从头部开始移动指针”和“从尾部部开始移动指针”两种情况

从头部开始移动指针

头头比对:

第一次比配,匹配后移动新老头指针:
image.png

第二次匹配,匹配后移动新老头指针:

image.png

直至老节点的头尾指针重合,此时,D 节点是 while 最后一次做比对:

image.png

比对完成后,指针继续后移,导致老节点的头指针越过尾指针,此时 while 循环结束;

while 循环结束时的指针状态如下:

image.png

此时,新节点的头指针指向的节点 E 为新增节点,后面可能还有 F G H 等新增节点,需要将它们( 指从 newStartIndex 到 newEndIndex 所有节点),添加到老节点儿子集合中

代码实现:

while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){ 
    // 头头比对:
    if(isSameVnode(oldStartVnode, newStartVnode)){ 
        // isSameVnode只能判断标签和key一样,但属性可能还有不同 
        // 所以需要patch方法递归更新新老虚拟节点的属性
        patch(oldStartVnode, newStartVnode); 
        // 更新新老头指针和新老头节点 
        oldStartVnode = oldStartVnode[++oldStartIndex]; 
        newStartVnode = newStartVnode[++newStartIndex]; 
    } 
}

// 1,新的多,插入新增的 
if(newStartIndex <= newEndIndex){ 
    // 新的开始指针和新的结束指针之间的节点 
    for(let i = newStartIndex; i <= newEndIndex; i++){ 
       // 获取对应的虚拟节点,并生成真实节点,添加到 dom 中 
       el.appendChild(createElm(newChildren[i])) 
    } 
}

测试效果:

let render1 = compileToFunction(`<div>
    <li key="A">A</li>
    <li key="B">B</li>
    <li key="C">C</li>
    <li key="D">D</li>
</div>`);

let render2 = compileToFunction(`<div>
    <li key="A" style="color:red">A</li>
    <li key="B" style="color:blue">B</li>
    <li key="C" style="color:yellow">C</li>
    <li key="D" style="color:pink">D</li>
    <li key="E">E</li>
    <li key="F">F</li>
</div>`);

更新前:

image.png

更新后:

image.png

备注:
将新儿子中新增的节点直接向后添加到老儿子集合中,使用 appendChild 即可
但是,如果新增的节点在头部,就不能用 appendChild 了,见下面尾尾比对分析;

从尾部开始移动指针

尾尾比对:

image.png

指针向前移动,当老节点的头尾指针重合,即 while 循环的最后一次比对:

image.png

比对完成指针向前移动后,循环结束时的指针状态如下:

image.png

while 比对完成后,需要将剩余新节点添加到老儿子中的对应位置

image.png

问题:如何向头部位置新增节点

问题:如何将新增节点 E、F 放到 A 的前面?

分析:

  • 要加到 A 节点前,不能继续使用 appendChild 向后追加节点
  • 前面的代码是指“从新的头指针到新的尾指针”这一区间的节点,即for (let i = newStartIndex; i <= newEndIndex; i++) 所以是先处理 E 节点,在处理 F 节点

先处理 E 节点,将 E 节点方到 A 节点前的位置:

image.png
再处理 F 节点,将 F 节点插入到 A 节点与 E 节点之间的位置:

image.png

当新增区域的头尾指针重合,即为最后一次处理;

方案:

新增节点有可能追加到后面,也有可能插入到前面

  • 头头比较时,将新增节点添加到老儿子集合中即可,使用 appendChild 追加
  • 尾尾比较时,

如何确认该向前还是向后添加节点?

要看 while 循环结束时,newChildren[newEndIndex + 1]新儿子的尾指针是否有节点
image.png

  • 如果有节点,说明是从尾向头进行比对的,新增节需要点添加到老儿子集合前面,使用insertBefore 插入指定位置
  • 如果无节点,说明是从头向尾进行比对的,新增节需要点追加到老儿子集合后面,使用 appendChild 追加

代码实现:

// 1,新的多(以新指针为参照)插入新增
if (newStartIndex <= newEndIndex) {
  // 新的开始指针和新的结束指针之间的节点
  for (let i = newStartIndex; i <= newEndIndex; i++) {
    // 判断当前尾节点的下一个元素是否存在:
    //  1,如果存在:则插入到下一个元素的前面
    //  2,如果不存在(下一个是 null) :就是 appendChild
    // 取参考节点 anchor:决定新节点放到前边还是后边
    //  逻辑:取去newChildren的尾部+1,判断是否为 null
    //  解释:如果有值说明是向前移动的,取出此虚拟元素的真实节点el,将新节点添加到此真实节点前即可
    let anchor = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el
    // 获取对应的虚拟节点,并生成真实节点,添加到 dom 中
    // el.appendChild(createElm(newChildren[i]))
    // 逻辑合并:将 appendChild 改为 insertBefore
    //  效果:既有appendChild又有insertBefore的功能,直接将参考节点放进来即可;
    //  解释:对于insertBefore方法,如果anchor为null,等同于appendChild;如果有值,则是insertBefore;
    el.insertBefore(createElm(newChildren[i]),anchor)
  }
}
备注:注意这里的 el.insertBefore 妙用,当 insertBefore 方法的第二个参数为 null 时,等同于 appendChild 方法

4,情况 2:老儿子比新儿子多,删除多余

let render1 = compileToFunction(`<div>
    <li key="A" style="color:red">A</li>
    <li key="B" style="color:blue">B</li>
    <li key="C" style="color:yellow">C</li>
    <li key="D" style="color:pink">D</li>
</div>`);

let render2 = compileToFunction(`<div>
    <li key="A" style="color:red">A</li>
    <li key="B" style="color:blue">B</li>
    <li key="C" style="color:yellow">C</li>
</div>`);

image.png

老的比新的多,在移动过程中就会出现:新的已经到头了时,老的还有

当移动结束时:老的头指针会和尾指针重合,新的头指针会越过新的尾指针

image.png

代码实现:

将老儿子集合,“从头指针到尾指针”区域的多余真实节点删除

// 2,老儿子比新儿子多,(以旧指针为参照)删除多余的真实节点
if(oldStartIndex <= oldEndIndex){
  for(let i = oldStartIndex; i <= oldEndIndex; i++){
    let child = oldChildren[i];
    el.removeChild(child.el);
  }
}

5,情况 3:反序情况

反序情况

image.png

这种情况下,可以使用“旧的头指针”和“新的尾指针”进行比较,即头尾比较

image.png

每次比较完成后,“旧的头指针”向后移动,“新的尾指针”向前移动

image.png

并且比较完成后,直接将老节点 A 放到老节点最后去

更确切的说,是插入到尾指针的下一个节点的前面(移动前,尾指针指向的 D 节点的下一个节点为 null)


继续比较 B,比较完成后移动指针
移动 B :插入到尾指针的下一个的前面(这时尾指针 D 的下一个是上一次移动过来的 A)

image.png

继续 C 和 C 比,之后再移动指针:
移动 C :插入到尾指针的下一个的前面(这时尾指针 D 的下一个是上一次移动过来的 B)

image.png

接下来比较 D,此时会发现“旧的头指针”和“新的头指针”一样了,都是 D

这时就比较完成了,D 无需再移动,结果就是 D C B A
(整个反序过程,共移动了3 次,移动而不是重新创建)

所以,对于反序操作来说,需要去比对头尾指针(老的头和新的尾),

每次比对完成后头指针向后移,尾指针向左移

代码部分,添加“头尾比较”逻辑:

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
  if (isSameVnode(oldStartVnode, newStartVnode)) {
    patch(oldStartVnode, newStartVnode);
    oldStartVnode = oldChildren[++oldStartIndex];
    newStartVnode = newChildren[++newStartIndex];
  }else if(isSameVnode(oldEndVnode, newEndVnode)){
    patch(oldEndVnode, newEndVnode);
    oldEndVnode = oldChildren[--oldEndIndex];
    newEndVnode = newChildren[--newEndIndex];
    // 头尾比较:老的头节点和新的尾节点做对比
  }else if(isSameVnode(oldStartVnode, newEndVnode)){
    // patch方法只会duff比较并更新属性,但元素的位置不会变化
    patch(oldStartVnode, newEndVnode);// diff:包括递归比儿子
    // 移动节点:将当前的节点插入到最后一个节点的下一个节点的前面去
    el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
    // 移动指针
    oldStartVnode = oldChildren[++oldStartIndex];
    newEndVnode = newChildren[--newEndIndex];
  }
}
注意:

要先插入节点,再移动指针
insertBefore是有移动效果的,会把原来的节点移走,这时 dom 的移动性
appendChild、insertBefore操作 dom 都有移动性,都会吧原来的 dom 移走

测试效果:

更新前:

image.png

更新后:

image.png

同理尾头比对的情况:

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (isSameVnode(oldStartVnode, newStartVnode)) {
      patch(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
    }else if(isSameVnode(oldEndVnode, newEndVnode)){
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
    }else if(isSameVnode(oldStartVnode, newEndVnode)){
      patch(oldStartVnode, newEndVnode);
      el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldChildren[++oldStartIndex];
      newEndVnode = newChildren[--newEndIndex];
    // 尾头比较
    }else if(isSameVnode(oldEndVnode, newStartVnode)){
      patch(oldEndVnode, newStartVnode);  // patch方法只会更新属性,元素的位置不会变化
      // 移动节点:将老的尾节点移动到老的头节点前面去
      el.insertBefore(oldEndVnode.el, oldStartVnode.el);// 将尾部插入到头部
      // 移动指针
      oldEndVnode = oldChildren[--oldEndIndex];
      newStartVnode = newChildren[++newStartIndex];
    }
}

测试效果:

let render1 = compileToFunction(`<div>
    <li key="E">E</li>
    <li key="A">A</li>
    <li key="B">B</li>
    <li key="C">C</li>
    <li key="D">D</li>
</div>`);

let render2 = compileToFunction(`<div>
    <li key="D" style="color:pink">D</li>
    <li key="C" style="color:yellow">C</li>
    <li key="B" style="color:blue">B</li>
    <li key="A" style="color:red">A</li>
</div>`);	

更新前:

image.png

更新后:

image.png


三,结尾

本篇,diff算法-比对优化(下),主要涉及以下几个点:

  • 介绍了儿子节点比较的流程
  • 介绍并实现了头头、尾尾、头尾、尾头4种特殊情况比对

下篇,diff算法-乱序比对


维护日志

20210805:

  • 添加了“从尾部开始移动指针”的图示
  • 添加了“问题:如何向头部位置新增节点”
  • 修改了部分有问题的图示
  • 修改了几处语义表达不够准确的地方

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

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

相关文章

墙裂推荐,2023年最强、最实用的IDEA插件推荐合集

插件目录Alibaba Java Coding Guidelines(阿里巴巴java开发规范)Alibaba Cloud AI Coding Assistant(阿里云AI代码助理)Code Glance3(代码地图)Codota AI Autocomplete for Java and JavaScriptCSDN Tools(CSDN官方插件)FindBugsGenerateAllSetter Postfix Completion (自动生成…

小程序uni-app的api

小程序uni-app的apiuni api简介uni api使用uni-app自定义组件—传统方式核心步骤uni-app自定义组件—easycom简介核心步骤uni-app组件库uViewUIuview介绍关键步骤uni api简介 uni-api 指的是uni-app 针对一些 微信小程序api所做的封装它解决了两个问题 原生的小程序api不支持…

C/C++const关键字详解(全网最全)

目录 1、const修饰普通变量 2、const修饰指针 &#xff08;1&#xff09;const修饰p: &#xff08;2&#xff09;const修饰*p&#xff1a; &#xff08;3&#xff09;const修饰p和*p 4、const修饰数组 5、const修饰函数形参 &#xff08;1&#xff09;const修饰普通形参…

【数据结构】6.4 图的存储结构

文章目录6.4.1 邻接矩阵&#xff08;数组&#xff09;表示法无向图的邻接矩阵无向图邻接矩阵的特点有向图的邻接矩阵有向图邻接矩阵的特点网&#xff08;有权图&#xff09;的邻接矩阵采用邻接矩阵创建无向网邻接矩阵的优缺点6.4.2 邻接表&#xff08;链式&#xff09;无向图的…

【人工智能原理自学】初识Keras:轻松完成神经网络模型搭建

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;笔记来自B站UP主Ele实验室的《小白也能听懂的人工智能原理》。 &#x1f514;本文讲解初识Keras&#xff1a;轻松完成神经网络模型搭建&#xff0c;一起卷起来叭&#xff01; 目…

Eureka入门

Eureka入门Eureka入门什么是Eureka构建项目demo服务拆分远程调用创建Pom聚合工程Eureka使用搭建注册中心注册服务远程调用出现的问题Eureka入门 什么是Eureka Eureka是SpringCloud提供的注册中心&#xff0c;用来解决微服务之间远程调用问题&#xff0c;如&#xff1a; 消费…

交通流的微观模型研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Redis原理篇(四)内存回收

Redis之所以性能强&#xff0c;最主要原因是基于内存存储。但是单节点的Redis其内存大小不宜过大&#xff0c;会影响持久化或主从同步性能。 可以通过配置文件来设置最大内存 # maxmemory <bytes> maxmemory 1gb一、过期策略 可以通过expire命令给Redis的key设置TTL …

【C++算法图解专栏】一篇文章带你掌握高精度加减乘除运算

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为 0 基础刚入门数据结构与算法的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们一起交流~ &#x1f4da;专栏地址&#xff1a;https://blog.csdn.net/Newin…

Java 异常 笔记

异常体系结构 异常分为Error和Exception。Error通常是灾难性错误&#xff0c;一般发生时&#xff0c;JVM选择终止程序执行&#xff1b;Exception通常可在程序中进行处理&#xff0c;尽量避免 Exception分支中有一个重要子类RuntimeException&#xff0c;运行时异常 ArrayInd…

数据库,计算机网络、操作系统刷题笔记34

数据库&#xff0c;计算机网络、操作系统刷题笔记34 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle…

深入理解Promise

Promise的前提概念 Promise是一个构造函数&#xff0c;用来生成Promise实例 Promise构造函数接受一个函数作为参数&#xff0c;该函数有两个参数&#xff0c;分别是resolve和reject resolve&#xff1a;成功时的回调 reject&#xff1a;失败时的回调 Promise分别有三个状态 1…

行人属性识别研究综述(一)

文章目录摘要1、简介2 问题的表述和挑战3 标准3.1 数据集3.2 评价标准4 行人属性识别的常规流程4.1 多任务学习4.2 多标签学习5 深度神经网络&#x1f407;&#x1f407;&#x1f407;&#x1f407;&#x1f407;&#x1f407;&#x1f407; 欢迎阅读 【AI浩】 的博客&#x1f…

C#上位机基础学习_基于S7.Net实现读取S7-1500PLC中的字符串变量

C#上位机基础学习_基于S7.Net实现读取S7-1500PLC中的字符串变量 如下图所示,首先在TIA博途中创建一个项目,添加一个1500PLC,添加一个DB块,在DB块中添加几个字符串变量, 如下图所示,打开Visual Studio 2019,新建一个项目,在Form1中添加一个按钮和一个文本框, 如下图…

linux——高级信号

高级信号的收发发&#xff1a;siquequ收&#xff1a;sigaction() 包含三个元素num,sigaction()函数&#xff0c;备份num ->signumsigaction是一个结构体&#xff0c;需额外配置再传进来备份直接忽略&#xff0c;代表不需要备份sigaction结构体又包含四个元素sa_handler&…

Git进阶:修改上次提交 git commit --amend

一、问题说明 git commit 后&#xff0c;发现刚才的备注写错了&#xff0c;或者代码漏掉了&#xff0c;这时我们肯定是想取消刚才的提交。此刻有两种方法 &#xff08;1&#xff09;使用git reset命令将刚才的提交会退掉。需要注意的是git reset --soft 和git reset --hard的区…

【附源码】国内首届Discord场景创意编程开源项目

以下开源项目是由环信联合华为举办的《国内首届Discord场景创意编程赛》作品&#xff0c;附源码&#xff0c;一键即用。 一、 模拟器游戏直播-新新人类 新新人类模拟器游戏直播基于环信超级社区Demo构建&#xff0c;增加以“video-x”命名的新型Channel&#xff0c;用户可在本…

Java三目运算符导致 NPE

在三目运算符中&#xff0c;表达式 1 和 2 在涉及算术计算或数据类型转换时&#xff0c;会触发自动拆箱。当其中的操作数为 null 值时&#xff0c;会导致 NPE 。 一、基础知识 三目运算符 三目运算符是 Java 语言中的重要组成部分&#xff0c;它也是唯一有 3 个操作数的运算…

Linux常用命令——tempfile命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) tempfile shell中给临时文件命名 补充说明 有时候在写Shell脚本的时候需要一些临时存储数据的才做&#xff0c;最适合存储临时文件数据的位置就是/tmp&#xff0c;因为该目录中所有的内容在系统重启后就会被清…

LInux(三)程序地址空间、内存管理

目录 一、程序地址空间 二、内存管理方式 1、分段式内存管理 2、分页式内存管理 3、段页式内存管理 三、关于内存管理内容补充&#xff08;分页式&#xff09; 1、页表简单呈现 2、访问权限位 3、缺页中断 4.内存置换算法 一、程序地址空间 创建父子进程同时访问同一变量…