简单Diff算法

news2024/11/29 10:47:25

简单Diff算法

渲染器的核心

Diff算法

解决的问题

比较新旧虚拟节点的子节点,实现最小化更新。

虚拟节点key属性的作用

就像虚拟节点的“身份证号”,在更新时,渲染器会通过key属性找到可复用的节点,然后尽可能地通过DOM移动操作来完成更新,避免过多地对 DOM 元素进行销毁和重建。key和type的属性值均都相同,则两个节点就是相同的,即可实现进行DOM的复用。

简单Diff算法地核心逻辑(如何寻找需要移动的节点)

拿新一组子节点中的节点去旧的一组子节点中去寻找可复用的节点。如果找到了,则记录该节点的位置索引。我们把这个索引称为最大索引。在整个更新过程中,如果一个节点的索引小于最大索引,则说明该节点需要移动。

节点的移动

使用的是insert方法,找到锚点元素进行插入操作,其中insert方法对于浏览器来说依赖于原生的insertBefore函数。

在这里插入图片描述

源码展示
function patchChildren(n1, n2, container) {
  if (typeof n2.children === 'string') {
    // 省略部分代码
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children
    // 用来存储寻找过程中遇到的最大索引值
    let lastIndex = 0;
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i];
      for (let j = 0; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j];
        if (newVNode.key === oldVNode.key) {
          // 进行打补丁
          patch(oldVNode, newVNode, container);
          if (j < lastIndex) {
            // 如果当前找到的节点在旧节点中的索引值小于最大索引值lastIndex,
            // 说明该节点对应的真实的DOM节点需要进行移
            // 先获取 newVNode 的前一个vnode,即 preVNode
            const preVNode = newChildren[i - 1]
            // 如果 prevVNode 不存在,则说明当前的 newVNode 是第一个节点,它不需要移动
            if (prevVNode) {
              // 由于我们需要将 newVNode 对应的真实DOM移动到 prevVNode 所对应的真实DOM后面,
              // 所以我们需要获取到 prevVNode 所对应的真实DOM的下一个兄弟节点,以此作为锚点
              
              const anchor = prevVNode.el.nextSibling();
              // 调用insert方法将 newVNode 对应的真实DOM插入到锚点元素的前面
              // 也就是 prevVNode 对应的真实DOM的后
              insert(newVNode.el, prevVNode.el, anchor);
            }
          } else {
            // 如果当前找到的节点在旧节点中的索引值大于或等于最大索引值lastIndex,
            // 则更新最大索引lastIndex的值
            lastIndex = j;
          }
          break;
        }
      }
    }
  }
}

上面代码中,如果j < lastIndex成立,则说明当前newVNode所对应的真实DOM节点需要移动。我们需要先获取当前 newVNode 节点的前一个虚拟节点,即newChildren[i - 1],然后使用insert函数完成节点的移动,其中insert 函数依赖浏览器原生的insertBefore函数。如下:

const renderer = createRenderer({
  // 省略部分代码
  insert(el, parent, anchor = null) {
    // insertBefore 需要锚点元素anchor
    parent.insertBefore(el, anchor);
  }
  // 省略部分代码
});
添加新元素

在新一组的子节点中对应的key没有在旧一组子节点中存在的节点,即为新节点。

在这里插入图片描述

如上图所示,p-4节点是一个需要新增的节点。在遍历的过程中能够发现p-4节点的key值在旧子节点中没有对应找到(视为新增节点),需要将p-4节点对应的真实DOM挂载在p-1节点对应的真实DOM节点的后面。

源码展示

function patchChildren(n1, n2, container) {
  if (typeof n2.children === 'string') {
    // 省略部分代码
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children;
    const newChildren = n2.children
    // 用来存储寻找过程中遇到的最大索引值
    let lastIndex = 0;
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i]
      // 在第一层循环中定义变量find,代表是否在旧的一组子节点中找到可以复用的节点
      // 初始值为false,代表没有找到
      let find = false;
      
      for (let j = 0; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j];
        if (newVNode.key === oldVNode.key) {
          
          // 找到可复用的节点,将find置为true
          find = true;
          
          // 进行打补丁
          patch(oldVNode, newVNode, container);
          if (j < lastIndex) {
            // 如果当前找到的节点在旧节点中的索引值小于最大索引值lastIndex,
            // 说明该节点对应的真实的DOM节点需要进行移
            // 先获取 newVNode 的前一个vnode,即 preVNode
            const preVNode = newChildren[i - 1]
            // 如果 prevVNode 不存在,则说明当前的 newVNode 是第一个节点,它不需要移动
            if (prevVNode) {
              // 由于我们需要将 newVNode 对应的真实DOM移动到 prevVNode 所对应的真实DOM后面,
              // 所以我们需要获取到 prevVNode 所对应的真实DOM的下一个兄弟节点,以此作为锚点
              
              const anchor = prevVNode.el.nextSibling();
              // 调用insert方法将 newVNode 对应的真实DOM插入到锚点元素的前面
              // 也就是 prevVNode 对应的真实DOM的后
              insert(newVNode.el, prevVNode.el, anchor);
            }
          } else {
            // 如果当前找到的节点在旧节点中的索引值大于或等于最大索引值lastIndex,
            // 则更新最大索引lastIndex的值
            lastIndex = j;
          }
          break;
        }
      }
      
      // 若代码运行至此,find仍为false
      // 说明当前 newVNode 没有在旧一组子节点找到可复用的节点
      // 即 newVNode 为新增节点,需要挂载
      if (!find) {
        // 为了将新增节点挂载到正确的位置,我们需要找到锚点元素
        // 获取 newVNode 的前一个 vnode 节点
        const prevVNode = newChildren[i - 1];
        let anchor = null;
        if (prevVNode) {
          // 如果有前一个 vnode 节点,则使用它的下一个兄弟节点作为锚点元素
          anchor = prevVNode.el.nextSibling;
        } else {
          // 如果没有前一个 vnode 节点,则说明即将挂载的节点是第一个子节点
          // 这时我们使用容器元素的 firstChild 作为锚点
          anchor = container.firstChild;
        }
        // 挂载 newVNode
         patch(null, newVNode, container, anchor);
      }
    }
  }
}

上面的代码,首先我们在外层循环中定义了find变量,它表示新一组子节点是否在旧一组子节点中找到可复用的节点。变量find初始值为false,一旦找到可复用的子节点就将find置为true。如果内层循环结束后,find值仍然为false,说明当前 newVNode 是一个新增节点,需要挂载。为了找到此节点被挂载的位置,我们要获取到锚点元素:找到 newVNode 前一个虚拟节点,即 prevNode。如果存在 prevNode 存在,那么我们就取 prevNode 节点的下一个兄弟节点对应的真实DOM元素作为锚点,进行挂载;如果不存在,则说明当前需要挂载的 newVNode 节点是第一个子节点,此时应该使用容器元素的container.firstChild作为锚点。最后将锚点 anchor 作为patch函数的第四个参数,调用 patch 函数进行挂载。

patch函数如下:

// patch 函数需要接收四个参数
// n1: 旧vnode
// n2: 新vnode
// container: 容器
// anchor: 锚点元素
function patch(n1, n2, container, anchor) {
  // 省略部分代码
  if (typeof type === 'string') {
    if (!n1) {
      // 挂载时将锚点元素作为第三个参数传递给 mountElement 函数
      mountElement(n2, container, anchor);
    } else {
      patchElement(n1, n2);
    }
  } else if (typeof type === Text) {
    // 省略部分代码
  } else if (typeof type === Fragment) {
    // 省略部分代码
  }
}

// mountElement 函数
function mountElement(vnode, container, anchor) {
  // 省略部分代码
  // 在插入节点时,将锚点元素透传给 insert 函数
  insert(el, container, anchor);
}

移除不存在的元素

在这里插入图片描述

如上图所示,节点p-2是需要被删除的元素。

源码展示

    function patchChildren(n1, n2, container) {
      if (typeof n2.children === 'string') {
        // 省略部分代码
      } else if (Array.isArray(n2.children)) {
        const oldChildren = n1.children;
        const newChildren = n2.children;

        // 用来存储寻找过程中遇到的最大索引值
        let lastIndex = 0;
        for (let i = 0; i < newChildren.length; i++) {
          // 省略部分代码
        }

        // 上一步的更新操作完成后,遍历旧的一组子节点
        for (let i = 0; i < oldChildren.length; i++) {
          const oldVNode = oldChildren[i];
          // 拿旧子节点 oldVNode 去新的一组子节点中寻找具有相同 key 值的节点
          const has = newChildren.find(ele => ele.key === oldVNode.key);
          if (!has) {
            // 如果没有找到具有相同 key 的节点,则说明需要删除该节点
            // 调用 unmount 函数将其卸载
            unmount(oldVNode);
          } else {
            // 省略部分代码
          }
        }
      }
    }

更新结束后,增加删除额外节点的逻辑来删除遗留节点。当基本的更新结束后,需要遍历旧的一组子节点,然后去新的一组子节点中去寻找具有相同 key 值的节点。如果找不到,则说明需要删除该节点(调用unmount函数将其卸载)。

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

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

相关文章

Spring Boot Admin健康检查引起的Spring Boot服务假死

问题现象 最近在spring boot项目中引入了 spring-boot-starter-actuator 后&#xff0c;测试环境开始出现服务假死的现象&#xff0c; 且这个问题十分怪异&#xff0c;只在多个微服务中的简称A的这个服务中出现&#xff0c;其他服务都没有出现这个问题&#xff0c; 之所以说…

Proxmox Backup Server(PBS)从2.X升级到PBS3

作者&#xff1a;田逸&#xff08;formyz&#xff09; 2023年11月31日&#xff0c;Proxmox 官方正式发布Proxmox Backup Server 3.1版本。现在我负责管理的Proxmox Backup Server&#xff08;以下简称PBS&#xff09;版本号为2.3&#xff0c;打算将部分PBS升级到PBS 3.1&#x…

微信小程序自定义步骤条效果

微信小程序自定义一个步骤条组件&#xff0c;自定义文字在下面&#xff0c;已完成和未完成和当前进度都不一样的样式&#xff0c;可点击上一步和下一步切换流程状态&#xff0c;效果如下。 这是视频效果&#xff1a; 前端实现步骤条效果 下面我们一步步实现编码&#xff0c;自定…

【拆盲盒02】算法题

题目 : 假设顺序表L中的元素按从小到大的次序排列&#xff0c;编写算法删除顺序表中“多余”的数据元素&#xff0c;即操作之后的顺序表中所有元素的值都不相同&#xff0c;要求时间尽可能的少&#xff1b;并对顺序表A&#xff08;1&#xff0c;1&#xff0c;2&#xff0c;2&am…

【js】js解析Token:

一、效果&#xff1a; 二、实现&#xff1a; export function getTokenObject(token) {//通过split()方法将token转为字符串数组,数组中的第二个字符进行解析return token ? JSON.parse(decodeURIComponent(escape(window.atob(token.split(".")[1].replace(/-/g &…

在 Spring 中操作 Redis

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信您对博主首页也很感兴趣o (ˉ▽ˉ&#xff1b;) &#x1f4dc;redis和缓存及相关问题和解决办法 什么是缓存预热、缓存穿透、缓存雪崩、缓存击穿 目录 1、引入依赖 2、对 Redis 的配置文件进行书写 3、S…

2022年中职组“网络安全”赛项湖南省B-3——私钥泄漏

B-3&#xff1a;应用服务漏洞扫描与利用 任务环境说明&#xff1a;需要环境有问题可以加q 服务器场景&#xff1a;Server15服务器场景操作系统&#xff1a;未知&#xff08;关闭链接&#xff09; 使用命令nmap探测目标靶机的服务版本信息&#xff0c;将需要使用的参数作为FLA…

UIToolKit使用心得

起因 因为那个uitoolkit自己写了一套graphView&#xff0c;所以想着来用用但是用完之后发现也不过如此 怎么构建自己的组件 我在继承Node之后想修改node的样式该怎么办呢是这样的。先用pick点击默认的node节点元素- 在pick默认创建的node节点之后&#xff0c;可以把它的uxml…

【产品设计】信息建设三驾马车:PLM系统拆解

本篇文章将介绍PLM的基础信息、发展及模块功能等内容&#xff0c;让大家对PLM有一个全面、完整地了解&#xff0c;方便在后期的工作中能快速地使用其解决方案&#xff0c;希望本篇文章能对你有所帮助。 PLM系统主要实现产品模块业务&#xff0c;既包含产品的创意设计、样品打样…

阶段十-分布式-docker虚拟化容器

第一章 Docker简介 1.2 节 Docker理念 Docker是基于Go语言实现的云开源项目&#xff1b;通过对应用组件的封装、分发、部署、运行等生命周期的管理&#xff0c;使用户的APP&#xff08;可以是一个WEB应用或数据库应用等等&#xff09;及其运行环境能够做到“一次封装&#xf…

《PCI Express体系结构导读》随记 —— 第I篇 第1章 PCI总线的基本知识(18)

接前一篇文章&#xff1a;《PCI Express体系结构导读》随记 —— 第I篇 第1章 PCI总线的基本知识&#xff08;17&#xff09; 1.4 PCI总线的中断机制 1.4.2 中断信号与PCI总线的连接关系 在PCI总线中&#xff0c;INTx信号属于边带信号。所谓边带信号是指这些信号在PCI总线环境…

错题总结五

一. 操作访问字节个数问题 这道题考察的是指针有关的知识&#xff0c;而这种指针的知识我们提到过 指针相关知识(入门)-CSDN博客 指针相关知识(进阶)-CSDN博客 在这里&#xff0c;我们复习一下。在指针里&#xff0c;int有4个字节&#xff0c;short有2个字节&#xff0c;换句…

jsp作用域

jsp四大域对象 page 只在当前页面有效&#xff0c;跳转后无效 request 服务器跳转有效&#xff0c;客户端发生跳转无效&#xff0c;&#xff08;因为客户端跳转&#xff0c;相当于发生两次跳转&#xff09; session 保存在浏览器会话中&#xff0c;服务器或客户端跳转均有…

Spring系列:Spring如何解决循环依赖

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

【ChatGPT 默认强化学习策略】PPO 近端策略优化算法

PPO 近端策略优化算法 PPO 概率比率裁剪 演员-评论家算法演员-评论家算法&#xff1a;多智能体强化学习核心框架概率比率裁剪&#xff1a;逐步进行变化的方法PPO 目标函数的设计重要性采样KL散度 PPO 概率比率裁剪 演员-评论家算法 论文链接&#xff1a;https://arxiv.org…

万字长文谈自动驾驶occupancy感知

文章目录 prologuepaper listVision-based occupancy :1. [MonoScene: Monocular 3D Semantic Scene Completion [CVPR 2022]](https://arxiv.org/pdf/2112.00726.pdf)2. [Tri-Perspective View for Vision-Based 3D Semantic Occupancy Prediction [CVPR 2023]](https://arxiv…

Android--Jetpack--Paging详解

不尝世间醋与墨&#xff0c;怎知人间酸与苦。 择一业谋食养命&#xff0c;等一运扭转乾坤。 你见过哪些令你膛目结舌的代码技巧&#xff1f; 文章目录 不尝世间醋与墨&#xff0c;怎知人间酸与苦。择一业谋食养命&#xff0c;等一运扭转乾坤。你见过哪些令你膛目结舌的代码技…

03.QT命名规范及快捷键(部分)

一、命名规范 1.类名 大驼峰规则&#xff1a;首字母大写&#xff0c;单词和单词之间首字母大写。 2.变量名 小驼峰规则&#xff1a;首字母小写&#xff0c;单词和单词之间首字母大写。 二、快捷键 1.代码操作相关 注释&#xff1a;ctrl / 运行&#xff1a;ctrl r 编译…

GPT4-AIl本地部署-chat AI本地使用

文章目录 GPT4-AIl本地部署GPT4客户端下载地址&#xff1a;对应的下载下载后的文件点击安装&#xff0c;改一下文件存放路径&#xff0c;下面都是默认下一步进度条100%后&#xff0c;点击完成 安装完桌面生成图标&#xff0c;点击选择都是NO&#xff0c;不进行数据上传点击后&a…

开源数据集的获取不该成为你的阻塞项

B站&#xff1a;啥都会一点的研究生公众号&#xff1a;啥都会一点的研究生 当可获取的数据有限&#xff0c;公共的开源数据集将是不错的选择 很多人并不知道该如何获取最权威的官方数据&#xff0c;亦或是不清楚有哪些数据集能为之所用&#xff0c;最适合自己的任务场景 不用…