手写vue-diff算法(三)updateChildren

news2024/12/29 8:43:58

前文回顾

上一篇提到,新老儿子节点比对可能存在的 3 种情况及对应的处理方法:

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

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

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

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

  • 情况 3:新老都有儿子

处理方法:执行diff比对,即:乱序比对;

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

优先对新老儿子节点的“头头、尾尾、头尾、尾头”节点进行比对,若均未能命中,最后再执行乱序比对;

diff算法-都有儿子节点

1.头头对比-新序列比老序列多节点-尾部

老:A B C D
新:A B C D E

1.1基本准备

在这里插入图片描述

function updateChildren(el, oldChildren, newChildren) {
    // 我们操作列表 经常会是有  push shift pop unshift reverse sort这些方法  (针对这些情况做一个优化)
    // vue2中采用双指针的方式 比较两个节点
    let oldStartIndex = 0;
    let newStartIndex = 0;
    let oldEndIndex = oldChildren.length - 1;
    let newEndIndex = newChildren.length - 1;

    let oldStartVnode = oldChildren[0];
    let newStartVnode = newChildren[0];

    let oldEndVnode = oldChildren[oldEndIndex];
    let newEndVnode = newChildren[newEndIndex];
}

1.2 循环分析

oldStartIndex > oldEndIndex说明是新的删除了部分节点
newStartIndex > newEndIndex说明是新的增加了部分节点
当不满足以上两种情况,进行头头、尾尾、头尾、尾头、乱序对比

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {}

1.3顺序对比

当新的头节点和旧的头节点相同时,调用patchVnode方法对子节点进行对比,同时进行属性更新,此处已经开始了递归调用
接下来将新旧头节点向后移动继续对比

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        if (isSameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldChildren[++oldStartIndex]
            newStartVnode = newChildren[++newStartIndex]
        }
}

1.4 新的添加了部分节点

可能增加了好多个节点
el:旧的子节点序列
使用createElm创建新的虚拟节点,插入到el

if (newStartIndex <= newEndIndex) {
        // push
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEL = createElm(newChildren[i])
            el.appendChild(childEL)
        }
    }

2.尾尾对比-新序列比老序列少节点-尾部

老:A B C D E
新:A B C D
el调用删除子节点方法removeChild,循环进行删除

if (oldStartIndex <= oldEndIndex) {
        // pop
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            let childEL = oldChildren[i].el
            el.removeChild(childEL)
        }
    }

3.头头对比-新序列比老序列多节点-头部

老:A B C D
新:E A B C D

从尾部进行对比,然后新旧尾节点逐渐减小

else if (isSameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldChildren[--oldEndIndex]
            newEndVnode = newChildren[--newEndIndex]
        }

此时插入方法需要进行修改,旧的插入方法时插入到尾部,此处需要插入到头部。

  • 如何判断是向前追加还是向后追加?
    向后追加尾指针后面没有节点,向前追加尾指针后面有节点,并且是插入到尾指针节点的前面
    if (newStartIndex <= newEndIndex) {
        // push
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEL = createElm(newChildren[i])
            let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; // 获取下一个元素
            // el.appendChild(childEL)
            el.insertBefore(childEL, anchor); 
        }
    }

4.尾尾对比-新序列比老序列少节点-尾部

老:A B C D
新:A B C D E
同2

5.交叉对比-老序列头部和新序列尾部相同

老:D E A B C
新:A B C D

老节点序列头部和新节点序列尾部相同
对比这两个节点,将老序列头部节点后移,新序列尾部节点向前移动

else if (isSameVnode(oldStartVnode, newEndVnode)) {
            patchVNode(oldEndVnode, newEndVnode);
            el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
            oldStartVnode = oldChildren[++oldStartIndex]
            newEndVnode = newChildren[--newEndIndex]
        }

6.交叉对比-老序列尾部和新序列头部相同

老:A B C D
新:D A B C

老节点序列尾部和新节点序列头部相同
在老序列节点头部插入新序列尾部节点
对比这两个节点,将老序列尾部节点前移,新序列头部节点向后移动

 else if (isSameVnode(oldEndVnode, newStartVnode)) {
            patchVNode(oldEndVnode, newStartVnode);
            el.insertBefore(oldEndVnode.el, oldStartVnode.el);
            oldEndVnode = oldChildren[--oldEndIndex]
            newStartVnode = newChildren[++newStartIndex]
        }

7.乱序对比

老:A B C D
新:B P A C D Q N
当新老序列不满足上面的六种情况后,以老节点序列作为对比序列,从新节点序列里面挨个拿出节点进行对比
B在老节点中有,则把B插入到oldStartVnode之前,并将B原来的位置标识为undefined,新指针指向P,老序列头指针不变,为A
P没有,则将P插入到oldStartVnode之前,新指针指向A,老序列头指针不变,为A
此时新老头指针相同,情况1,新老头指针向后移动,

    function makeIndexByKey(children) {
        let map = {

        }
        children.forEach((child, index) => {
            map[child.key] = index;
        });
        return map;
    }

    let map = makeIndexByKey(oldChildren);
else {
            // 在给动态列表添加key的时候 要尽量避免用索引,因为索引前后都是从0 开始 , 可能会发生错误复用 
            // 乱序比对
            // 根据老的列表做一个映射关系 ,用新的去找,找到则移动,找不到则添加,最后多余的就删除
            let moveIndex = map[newStartVnode.key]; // 如果拿到则说明是我要移动的索引
            if (moveIndex !== undefined) {
                let moveVnode = oldChildren[moveIndex]; // 找到对应的虚拟节点 复用
                el.insertBefore(moveVnode.el, oldStartVnode.el);
                oldChildren[moveIndex] = undefined; // 表示这个节点已经移动走了
                patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
            } else {
                el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
            }
            newStartVnode = newChildren[++newStartIndex];
        }

code

function updateChildren(el, oldChildren, newChildren) {
    // 我们操作列表 经常会是有  push shift pop unshift reverse sort这些方法  (针对这些情况做一个优化)
    // vue2中采用双指针的方式 比较两个节点
    let oldStartIndex = 0;
    let newStartIndex = 0;
    let oldEndIndex = oldChildren.length - 1;
    let newEndIndex = newChildren.length - 1;

    let oldStartVnode = oldChildren[0];
    let newStartVnode = newChildren[0];

    let oldEndVnode = oldChildren[oldEndIndex];
    let newEndVnode = newChildren[newEndIndex];


    function makeIndexByKey(children) {
        let map = {

        }
        children.forEach((child, index) => {
            map[child.key] = index;
        });
        return map;
    }

    let map = makeIndexByKey(oldChildren);




    // 循环的时候为什么要+key
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止  || 有一个为true 就继续走
        // 双方有一方头指针,大于尾部指针则停止循环
        if (!oldStartVnode) {
            oldStartVnode = oldChildren[++oldStartIndex]
        } else if (!oldEndVnode) {
            oldEndVnode = oldChildren[--oldEndIndex]
        } else if (isSameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
            // 比较开头节点
        } else if (isSameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
            // 比较开头节点
        } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);
            // insertBefore 具备移动性 会将原来的元素移动走
            el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
            oldEndVnode = oldChildren[--oldEndIndex];
            newStartVnode = newChildren[++newStartIndex];
        }
        else if (isSameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);
            // insertBefore 具备移动性 会将原来的元素移动走
            el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 将老的尾巴移动到老的前面去
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex];
        } else {
            // 在给动态列表添加key的时候 要尽量避免用索引,因为索引前后都是从0 开始 , 可能会发生错误复用 
            // 乱序比对
            // 根据老的列表做一个映射关系 ,用新的去找,找到则移动,找不到则添加,最后多余的就删除
            let moveIndex = map[newStartVnode.key]; // 如果拿到则说明是我要移动的索引
            if (moveIndex !== undefined) {
                let moveVnode = oldChildren[moveIndex]; // 找到对应的虚拟节点 复用
                el.insertBefore(moveVnode.el, oldStartVnode.el);
                oldChildren[moveIndex] = undefined; // 表示这个节点已经移动走了
                patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
            } else {
                el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
            }
            newStartVnode = newChildren[++newStartIndex];
        }

    }
    if (newStartIndex <= newEndIndex) { // 新的多了 多余的就插入进去
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEl = createElm(newChildren[i])
            // 这里可能是像后追加 ,还有可能是向前追加
            let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; // 获取下一个元素
            // el.appendChild(childEl);
            el.insertBefore(childEl, anchor); // anchor 为null的时候则会认为是appendChild
        }
    }

    if (oldStartIndex <= oldEndIndex) { // 老的对了,需要删除老的
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            if (oldChildren[i]) {
                let childEl = oldChildren[i].el
                el.removeChild(childEl);
            }
        }
    }

    // 我们为了 比较两个儿子的时候 ,增高性能 我们会有一些优化手段
    // 如果批量像页面中修改出入内容 浏览器会自动优化 
}

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

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

相关文章

基础知识--客户端·服务端·代理

目录 一、客户端 1.什么是客户端 2.客户端分类 二、服务端 1.什么是服务器 2.服务器的作用 3.服务器工作原理 4.服务器的组成 服务器硬件 服务器软件 5.补充 三、代理 1.代理的分类 正向代理 反向代理 两者的区别与联系 2.总结 一、客户端 1.什么是客户端 …

【八股】【C++】(二)函数、类、模板

这里写目录标题 形参与实参的区别函数调用过程指针和引用当函数参数回调函数友元函数重载匹配运算符重载直接初始化与拷贝初始化函数指针C中struct&#xff08;结构&#xff09;和class&#xff08;类&#xff09;的区别C有哪几种构造函数构造函数的执行顺序析构函数的执行顺序…

设计消息模块的业务层Web层

目录 业务层 一、定义Message业务接口 二、定义Message业务实现类 Web层 一、获取分页消息列表 二、根据ID查询消息 三、把未读消息更新成已读消息 四、删除消息 业务层 一、定义Message业务接口 创建 MessageService.java 类 public interface MessageService {pub…

mybatis-plus使用@Delete注解批量删除实战

使用Delete注解批量删除 1、控制器调用 // test // http://localhost:3000/function/test // 删除操作按钮权限 Transactional GetMapping("/test") public JSONObject testBatch() {// Arrays.asList(1, 2, 3)JSONObject result new JSONObject();try {functionM…

Django+vue3权限菜单rabc设计和动态路由

本次是基于Django和vue实现 github源码&#xff1a;nineaiyu/xadmin-server: xadmin-基于Djangovue3的rbac权限管理系统 (github.com) 服务器设计及部分代码 权限控制的话&#xff0c;可以基于Django的permission进行控制&#xff0c;并通过访问api的URL操作 核心代码如下 …

stable-diffusion使用openpose报错

依据教程 &#xff1a; https://post.smzdm.com/p/awz2l2xg/ 使用 stable-diffusion教学之ControlNetlora换脸 报错&#xff1a; urllib.error.URLError: <urlopen error [WinError 10054] 远程主机强迫关闭了一个现有的连接。> File "E:\ai\sd-webui-aki-v4\ext…

【数据挖掘】时间序列教程【五】

(说明:本文接上回:【数据挖掘】时间序列教程【四】_无水先生的博客-CSDN博客) 上面介绍的傅里叶变换的问题在于,无论是正弦/余弦回归模型形式还是复指数形式,它都需要 操作以计算所有傅里叶系数。有n 数据点和有n/2 可以计算傅里叶系数的频率。每个频率系…

Springboot3新特性异常信息ProblemDetail详解

环境&#xff1a;Springboot3.0.5 概述 RFC 7807定义了为HTTP响应中错误的可读详细信息&#xff0c;以避免需要为HTTP API定义新的错误响应格式。HTTP [RFC7230]状态码有时不足以传达关于错误的足够信息。 RFC 7807 定义了简单的JSON[RFC7159]和XML[W3C.REC-XML-20081126]文…

7.MHA高可用配置及故障切换

文章目录 MHA高可用配置及故障切换MHA概念实验配置时间同步与主从复制安装MHA服务SSH免交互认证验证MHA服务是否开启启动服务 故障模拟恢复故障过程 MHA高可用配置及故障切换 MHA概念 MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切…

计网实验第四章:IP

问题集1&#xff1a; 1.192.168.31.7 2. 上层协议为ICMP&#xff0c;并且其字段值为1 3. 20字节 56字节 - 20字节 36字节 解释&#xff1a;如图所示 4. 这个报文段没有分段发送 原因如图&#xff1a;按照下图所示 标志位显示没有更多的分段。 5. 同一地址发送的这两个…

1.计算机是如何工作的(下)

文章目录 4.编程语言&#xff08;Program Language&#xff09;4.1程序&#xff08;Program&#xff09;4.2早期编程4.3编程语言发展 5.操作系统&#xff08;Operating System&#xff09;5.1操作系统的定位5.2什么是进程/任务&#xff08;Process/Task&#xff09;5.3进程控制…

[LangChain]简介快速入门

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;人工智能。 目录 1、简介2、快速入门2.1、LLMs2.2、聊天模型2.3、提示模板2.4、链2.5、代理2.6、内存 1、简介 …

1758_C语言通过预处理的eror输出异常信息

全部学习汇总&#xff1a; GreyZhang/c_basic: little bits of c. (github.com) 这个功能我一直没有使用过&#xff0c;能够想到它完全是因为之前从某些代码中看了一眼。或许&#xff0c;这就是我跟这个小小知识点的缘分。 最近想处理一个可以适配多种情况的程序&#xff0c;…

领域驱动设计(DDD,Domain-Driven Design)

领域驱动设计 前言正文领域驱动设计基本概念什么是领域模型&#xff1f;什么是领域服务&#xff08;Domain Service&#xff09;&#xff1f;什么是领域事件&#xff1f; 秒杀项目中的领域分析一、秒杀活动领域设计秒杀活动领域模型领域服务领域事件 二、秒杀品领域设计领域模型…

开源自动化测试框架介绍

开源自动化测试框架介绍 一、Junit&#xff08;白盒测试、API自动化、UI自动化&#xff09;【官网】【简介】【使用场景】 二、Selenium&#xff08;Web自动化、爬虫&#xff09;【官网】【简介】【使用场景】 三、TestNG&#xff08;白盒测试、API自动化、UI自动化&#xff09…

Linux文件系统架构和共享文件方法

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天来聊聊 Linux文件系统架构和共享文件方法。 在Linux环境中使用文件和目录是工作中不可回避的环节。当然&#xff0c;在我的博客里成立windows程序员看linux这个专题&#xff0c;主要还是因为微软也发布了…

LwIP系列(4):ARP协详解

前言 对于应用程序而言&#xff0c;我们与其他设备、服务通信&#xff0c;主要通过域名、IP进行通信&#xff0c;而以太网底层驱动&#xff0c;最终是通过MAC地址来表示设备的唯一标识&#xff0c;即IP是逻辑地址&#xff0c;并不是物理地址。在上一篇文章中&#xff0c;我们也…

【ArcGIS Pro二次开发】(45):手搓一个工具执行进度框

我在之前做的工具中&#xff0c;UI这部分基本没怎么深入&#xff0c;都是直接用的现成的控件。 其中有一个问题比较突出&#xff0c;就是没有工具执行的进度框提示。曾经也用过系统自带的信息提示框和进度条&#xff0c;但太简陋&#xff0c;确实不好用。于是就想抄一个进度框…

Linux 读文件 - readahead预读算法

顺序读场景 intmain{charc[ 4096];intin -1;in open( "news.txt", O_RDONLY);intindex 0;while(read(in, &c, 4096) 4096){printf( "index: %d,len: %ld.\n",index, strlen(c));memset(c, 0, sizeof(c));index;} 数据结构 /** Track a single fi…

CentOS7 静默方式安装 Oracle19C

CentOS7 静默方式安装 Oracle19C 操作系统&#xff1a;CentOS7 Oracle&#xff1a; 19C 安装常用工具和依赖 yum -y install vim tar net-tools wget perl python3 readline* deltarpm python-deltarpm \zip unzip bc compat-libcap1* compat-libcap* binutils compat-libstdc…