简单Diff算法

简单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.mfbz.cn/a/288881.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Hexo 部署 Github Pages, Github Actions自动部署

想整个静态的博客部署在github pages 历经两天的折磨终于是摸索成功了&#xff0c;官网的文档太简陋了&#xff0c;很多东西没说清楚。 欢迎大家访问我的博客&#xff01; CanyueThis is Canyues blog.https://mobeicanyue.github.io/ 最终实现的效果&#xff0c;一个项目仓库…

polar CTF 简单rce

一、题目 <?php /*PolarD&N CTF*/ highlight_file(__FILE__); function no($txt){if(!preg_match("/cat|more|less|head|tac|tail|nl|od|vim|uniq|system|proc_open|shell_exec|popen| /i", $txt)){return $txt;}else{ die("whats up");}} $yyds(…

【openGauss服务器端工具的使用】

【openGauss服务器端工具的使用】 gs_checkperf openGauss 不仅提供了gs_checkperf工具来帮助用户了解openGauss的负载情况。 使用数据库安装用户登录服务器&#xff0c;执行如下命令进行查看数据库性能&#xff1a; 简要信息展示&#xff1a;[ommopengauss03 ~]$ gs_checkperf…

Ubuntu Server 22.04 连接Wifi并配置静态IP

Ubuntu Server 22.04 连接Wifi并配置静态IP 前言&#xff1a;我家最近好几台电脑&#xff0c;我都想跑着Ubuntu Server做服务器&#xff0c;但是近几年的超级本已经不自带网口了&#xff0c;所以我就考虑用Wifi来联网&#xff0c;速度也还可以&#xff0c;但是既然是跑服务&…

《算法导论》复习——CHP1、CHP2 算法基础

基本定义&#xff1a; 算法是一组有穷的规则&#xff0c;规定了解决某一特定类型问题的一系列运算。 关心算法的正确性和效率。 算法的五个重要特性&#xff1a;确定性、能行性、输入、输出、有穷性。 基础方法&#xff1a; 伪代码&#xff08;Pseudocode&#xff09;&#xff…

Springboot集成RabbitMq二

接上一篇&#xff1a;Springboot集成RabbitMq一-CSDN博客 1、搭建项目-消费者 与之前一样 2、创建配置类 package com.wym.rabbitmqconsumer.utils;import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.spring…

11.盛水最多的容器(双指针,C解法)

题目描述&#xff1a; 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;…

床垫选得好孩子睡得香!康姿百德学生床垫让孩子拥有甜美梦乡

睡眠与健康密切相关,而床垫的选购则直接关系到人们睡眠质量的好坏。而孩子的床垫选择更是重中之重,青少年儿童正处于生长发育的重要时期,床垫选不好很容易导致孩子睡眠不足,影响孩子的学习,甚至会影响孩子的脊椎发育。所以,给孩子挑张好用又合适的床垫十分重要,现在让我们来看看…

SpringMVC-域对象共享数据

一、request域对象共享数据 1.1 通过ServletAPI共享数据 RequestMapping("/servletAPI")public String servletAPI(HttpServletRequest request){request.setAttribute("requestAttribute","helloworld");return "servletAPI";}<!…

(学习打卡1)重学Java设计模式之设计模式介绍

前言&#xff1a;听说有本很牛的关于Java设计模式的书——重学Java设计模式&#xff0c;然后买了(*^▽^*) 开始跟着小傅哥学Java设计模式吧&#xff0c;本文主要记录笔者的学习笔记和心得。 打卡&#xff01;打卡&#xff01; 设计模式介绍 一、设计模式是什么&#xff1f; …

学习JavaEE的日子 day08 方法的重载,递归,万年历

day08 1.方法的重载 >理解&#xff1a;方法与方法之间的关系> 条件&#xff1a;> 1.方法必须在同一个类中> 2.方法名必须一致> 3.参数列表的个数或者类型不一致> 4.与返回值无关> 好处&#xff1a;系统会根据具体实参类型自动匹配到对应的方法…

React(2): 使用 html2canvas 生成图片

使用 html2canvas 生成图片 需求 将所需的内容生成图片div 中包括 svg 等 前置准备 "react": "^18.2.0","react-dom": "^18.2.0","html2canvas": "^1.4.1",实现 <div ref{payRef}></div>const pa…

阿里云性能测评ESSD Entry云盘、SSD云盘、ESSD和高效云盘

阿里云服务器系统盘或数据盘支持多种云盘类型&#xff0c;如高效云盘、ESSD Entry云盘、SSD云盘、ESSD云盘、ESSD PL-X云盘及ESSD AutoPL云盘等&#xff0c;阿里云百科aliyunbaike.com详细介绍不同云盘说明及单盘容量、最大/最小IOPS、最大/最小吞吐量、单路随机写平均时延等性…

循环与基础函数

循环与函数 1.循环的三种方式2.循环的中断与空语句3.函数的定义与使用4.参数的作用域5.指针6.总结 1.循环的三种方式 我们最熟悉的循环为for和while&#xff0c;这两种循环方式在Python系列介绍过。在C中&#xff0c;循环的基本逻辑同Python是类似的。c中while循环的语法如下&…

案例088:基于微信小程序的校车购票平台设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

thumbnailator 基本使用教程

thumbnailator 基本使用教程 本文中的 Demo 项目使用 SpringBoot 创建&#xff0c;代码仓库地址: thumbnailator-study: 使用 Thumbnailator 库的 Demo 程序&#xff0c;演示地址: www.huhailong.vip/thumbnailator-study。我的站点。) 使用 thumbnailator 库来操作图片非常的…

大语言模型LLM微调技术:P-Tuning

1 引言 Bert时代&#xff0c;我们常做预训练模型微调&#xff08;Fine-tuning&#xff09;&#xff0c;即根据不同下游任务&#xff0c;引入各种辅助任务loss和垂直领域数据&#xff0c;将其添加到预训练模型中&#xff0c;以便让模型更加适配下游任务的方式。每个下游任务都存…

基于华为ENSP模拟器-vlan划分网络

需求 不连外网的内网。需求隔离故障和隔离广播风暴&#xff0c;并要保证网络的连通。 解决方案使用三层交互机&#xff0c;设置vlan用于隔离网络&#xff0c;并在三层交互机为网关保证各个vlan之间的通讯。 实现 使用三层交互机&#xff0c;设置vlan用于隔离网络&#xff0…

广州怎么找工作哪里工作机会多

广州找工作上 吉鹿力招聘网 打开 吉鹿力招聘网 “注册账号”&#xff0c;然后输入个人基本信息&#xff0c;进行注册&#xff08;可使用手机号注册&#xff0c;也可以使用邮箱注册&#xff09;。 填写求职意向&#xff0c;基本信息点击“下一步”。 填写工作经历点击“下一步”…

【感知机】感知机(perceptron)学习算法的原始形式

感知机( perceptron )是二类分类的线性分类模型&#xff0c;其输入为实例的特征向量&#xff0c;输出为实例的类别&#xff0c;取1 和-1二值。感知机对应输入空间(特征空间)中将实例划分为正负两类的分离超平面&#xff0c;是一种判别模型。感知机是神经网络与支持向量机的基础…