vuejs 设计与实现 - 简单diff算法

news2024/12/23 2:00:21

DOM 复用与key的作用:

DOM 复用什么时候可复用?

  • key 属性就像虚拟节点的“身份证”号,只要两个虚拟节点的 type属性值和 key 属性值都相同,那么我们就认为它们是相同的,即可以进行 DOM 的复用。即 我们通过【移动】来操作dom,而不是删除dom,创建dom。这样会更节省性能。

如下图展示了有key和无key时新旧两组子节点的映射情况:
请添加图片描述
如上图可知:如果没有 key,我们无法知道新子节点与旧子节点 间的映射关系,也就无法知道应该如何移动节点。有 key 的话情况则 不同,我们根据子节点的 key 属性,能够明确知道新子节点在旧子节 点中的位置,这样就可以进行相应的 DOM 移动操作了。

强调:DOM 可复用并不意味着不需要更新.如下所示的2个虚拟节点:

const oldVNode = { type: 'p', key: 1, children: 'text 1' }
const newVNode = { type: 'p', key: 1, children: 'text 2' }

这两个虚拟节点拥有相同的 key 值和 vnode.type 属性值。这意 味着, 在更新时可以复用 DOM 元素,即只需要通过移动操作来完成更 新。但仍需要对这两个虚拟节点进行打补丁操作,因为新的虚拟节点 (newVNode)的文本子节点的内容已经改变了(由’text 1’变成 ‘text 2’)。因此,在讨论如何移动DOM之前,我们需要先完成打补丁操作.

本节以下面的节点为例,进行简单diff算法:

 const oldVNode = {
     type: 'div',
     children: [
         { key: 1, type: 'p', children: '1' },
         { key: 2, type: 'p', children: '2' },
         { key: 3, type: 'p', children: '3' },
     ]
 }

 const newVNode = {
     type: 'div',
     children: [
         { key: 3, type: 'p', children: '3' },
         { key: 2, type: 'p', children: '2' },
         { key: 1, type: 'p', children: '1' },
     ]
 }

每一次寻找可复用的节点时,都会记录该可复用 节点在旧的一组子节点中的位置索引。

找到需要移动的元素

// 1.找到需要移动的元素
function patchChildren(n1, n2) {
    const oldChildren = n1.children
    const newChildren = n2.children

    let lastIndex = 0
    for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        for (j = 0; j < oldChildren.length; j++) {
            const oldVNode = oldChildren[j]
            if (newVNode.key === oldVNode.key) {
				
				// 移动DOM之前,我们需要先完成打补丁操作
				patch(oldVNode, newVNode, container)
                
                if (j < lastIndex) {
                    console.log('需要移动的节点', newVNode, oldVNode, j)

                } else {
                    lastIndex = j
                }
                break;
            }
        }
    }
}
patchChildren(oldVNode, newVNode)

请添加图片描述

如何移动元素

更新的过程:

第一步:取新的一组子节点中第一个节点 p-3,它的 key 为 3,尝试在旧的一组子节点中找到具有相同 key 值的可复用节点。发现能够找到,并且该节点在旧的一组子节点中的索引为 2。此时变量 lastIndex 的值为 0,索引 2 不小于 0,所以节点 p-3 对应的真实 DOM 不需要移动,但需要更新变量 lastIndex 的值为2。

第二步:取新的一组子节点中第二个节点 p-1,它的 key 为 1,尝试在旧的一组子节点中找到具有相同 key 值的可复用节点。发
现能够找到,并且该节点在旧的一组子节点中的索引为 0。此时变量 lastIndex 的值为 2,索引 0 小于 2,所以节点 p-1 对应的真实 DOM 需要移动。

到了这一步,我们发现,节点 p-1 对应的真实 DOM 需要移动,但应该移动到哪里呢?我们知道, children的顺序其实就是更新后真实DOM节点应有的顺序。所以p-1在新children 中的位置就代表了真实 DOM 更新后的位置。由于节点p-1在新children中排在节点p-3后面,所以我们应该把节点p-1 所对应的真实DOM移到节点p-3所对应的真实DOM后面。

可以看到,这样操作之后,此时真实 DOM 的顺序为 p-2、p-3、p-1。

第三步:取新的一组子节点中第三个节点 p-2,它的 key 为 2。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点。发现能够找到,并且该节点在旧的一组子节点中的索引为 1。此时变量 lastIndex 的值为 2,索引 1 小于 2,所以节点 p-2 对应的真实 DOM 需要移动。

如下图移动节点:
请添加图片描述

第三步与第二步类似,节点 p-2 对应的真实 DOM 也需要移动。 面后同样,由于节点 p-2 在新 children 中排在节点 p-1 后面,所以我们应该把节点 p-2 对应的真实 DOM 移动到节点 p-1 对应的真实DOM 后面。移动后的结果如图下图所示:
请添加图片描述

经过这一步移动操作之后,我们发现,真实 DOM 的顺序与新的一组子节点的顺序相同了:p-3、p-1、p-2。至此,更新操作完成。

function patchChildren(n1, n2) {
    const oldChildren = n1.children
    const newChildren = n2.children

    let lastIndex = 0
    for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        for (j = 0; j < oldChildren.length; j++) {
            const oldVNode = oldChildren[j]
            if (newVNode.key === oldVNode.key) {
				// 移动DOM之前,我们需要先完成打补丁操作
				patch(oldVNode, newVNode, container)
               
                if (j < lastIndex) {
                    // console.log('需要移动的节点', newVNode, oldVNode, j)
					
					 // 如何移动元素
                    const prevVNode = newChildren[i - 1]
                    if (prevVNode) {
                            // 2.找到 prevVNode 所对应真实 DOM 的下一个兄 弟节点,并将其作为锚点
                            const anchor = prevVNode?.el?.nextSibling

                            console.log('插入', prevVNode, anchor)
                    }


                } else {
                    lastIndex = j
                }
                break;
            }
        }
    }
}
patchChildren(oldVNode, newVNode)

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

添加新元素

请添加图片描述

function patchChildren(n1, n2) {
  const oldChildren = n1.children
  const newChildren = n2.children

  let lastIndex = 0
  
  for (let i = 0; i < newChildren.length; i++) {
      
      // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点
      let find = false
      
      const newVNode = newChildren[i]
      
      for (j = 0; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          if (newVNode.key === oldVNode.key) {
              // 一旦找到可复用的节点,则将变量 find 的值设为 true
              find = true
              
              if (j < lastIndex) {
                  // console.log('需要移动的节点', newVNode, oldVNode, j)

                  const prevVNode = newChildren[i - 1]
                  if (prevVNode) {
                          // 2.找到 prevVNode 所对应真实 DOM 的下一个兄 弟节点,并将其作为锚点
                          const anchor = prevVNode?.el?.nextSibling

                          console.log('插入', prevVNode, anchor)
                  }


              } else {
                  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)
      }
  }
}
patchChildren(oldVNode, newVNode)

移除不存在的元素

// 4.移除不存在的元素
function patchChildren(n1, n2) {
    const oldChildren = n1.children
    const newChildren = n2.children

    let lastIndex = 0
    
    for (let i = 0; i < newChildren.length; i++) {
        
        // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点
        let find = false
        
        const newVNode = newChildren[i]
        
        for (j = 0; j < oldChildren.length; j++) {
            const oldVNode = oldChildren[j]
            if (newVNode.key === oldVNode.key) {
                // 一旦找到可复用的节点,则将变量 find 的值设为 true
                find = true
                
                if (j < lastIndex) {
                    // console.log('需要移动的节点', newVNode, oldVNode, j)

                    const prevVNode = newChildren[i - 1]
                    if (prevVNode) {
                            // 2.找到 prevVNode 所对应真实 DOM 的下一个兄 弟节点,并将其作为锚点
                            const anchor = prevVNode?.el?.nextSibling

                            console.log('插入', prevVNode, anchor)
                    }


                } else {
                    lastIndex = j
                }
                break;
            }
        }

        // 如果代码运行到这里,find 仍然为 false,说明当前newVNode没有在旧的一组子节点中找到可复用的节点,也就是说,当前newVNode是新增节点,需要挂载
        if (!find) {
            const prevVNode = newChildren[i - 1] 
        }
    }

    

    // 移除不存在的元素
    for (let i = 0; i < oldChildren.length; i++) {
        const oldVNode = oldChildren[i]
        const has = newChildren.find(vnode => vnode.key === oldVNode.key)
        
        // 如果没有找到具有相同 key 值的节点,则说明需要删除该节点
        if (!has) {

            // 调用 unmount 函数将其卸载
            unmount(oldVNode)
        }
    }
}
patchChildren(oldVNode, newVNode)

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

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

相关文章

C 语言的 pow() 函数

作用: Calculates x raised to the power of y. 函数原型: double pow( double x, double y ); Required Header: <math.h> Compatibility: ANSI Return Value pow returns the value of x y x^{y} xy. No error message is printed on overflow or underflow. Paramete…

【数据结构刷题】数组oj

前言:本文章是关于在力扣上面的数组相关面试题的讲解&#xff0c;包括:1.原地移除数组中所有的元素val&#xff0c;要求时间复杂度为O(N)&#xff0c;空间复杂度为O(1),2.删除排序数组中的重复项。3. 合并两个有序数组。一.原地移除数组中所有的元素val 题目: https://leetcod…

RT-Thread系列09——Finsh自定义命令

文章目录 1. 无参数命令2. 带参数命令2.1. 字符串参数2.2. 数字型参数 >>> 文章汇总&#xff08;有代码汇总&#xff09; <<< 目标&#xff1a;使用Finsh自定义命令。 RT-Thread studio&#xff0c;版本: 2.2.6。RT-Thread&#xff1a;标准版&#xff0c;4…

《Java-SE-第三十二章》之模拟实现HTTP服务器

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

Flask实现接口mock,安装及使用教程(一)

1、什么是接口mock 主要是针对单元测试的应用&#xff0c;它可以很方便的解除单元测试中各种依赖&#xff0c;大大的降低了编写单元测试的难度 2、什么是mock server 正常情况下&#xff1a;测试客户端——测试——> 被测系统 ——依赖——>外部服务依赖 在被测系统和…

CVE漏洞复现-CVE-2021-3493 Linux 提权内核漏洞

CVE-2021-3493 Linux 提权内核漏洞 漏洞描述 CVE-2021-3493 用户漏洞是 Linux 内核中没有文件系统中的 layfs 中的 Ubuntu over 特定问题&#xff0c;在 Ubuntu 中正确验证有关名称空间文件系统的应用程序。buntu 内核代码允许低权限用户在使用 unshare() 函数创建的用户命名…

象棋中“马”的题目(深搜)

题面 题目描述 中国象棋半张棋盘如图&#xff08;a&#xff09;所示。马自左下角往右上角跳。 今规定只许往右跳&#xff0c;不许往左跳&#xff0c;且要求马跳的方式按照&#xff08;b&#xff09;图顺时针深度优先递归。比如图&#xff08;a&#xff09;中所示为一种跳行路线…

使用动态规划实现错排问题-2023年全国青少年信息素养大赛Python复赛真题精选

[导读]&#xff1a;超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲&#xff0c;这是超平老师解读Python编程挑战赛真题系列的第15讲。 全国青少年信息素养大赛&#xff08;原全国青少年电子信息智能创新大赛&#xff09;是“世界机器人大会青少年机器人设…

Netty:服务端通过ServerBootstrap的childHandler函数设置处理客户端的ChannelHandler

说明 服务端通过io.netty.bootstrap.ServerBootstrap启动&#xff0c;ServerBootstrap的 childHandler(ChannelHandler childHandler)函数用于增加处理客户端的ChannelHandler。这个childHandler一般是ChannelInitializer的子类&#xff0c;用于配置ChannelPipeline&#xff0…

GD32F103VE串口与DMA传输

GD32F103VE串口与DMA传输&#xff0c;本测试采用的的串口1和DMA0之间的数据传输&#xff0c;然后通过RS485和其它设备进行数据交换&#xff0c;没有采用任何中断参与。 GD32F103VE的DMA0请求映射到串口&#xff1a; 1&#xff0c;USART0_RX映射到DMA0的通道4&#xff0c;USART…

springboot 多模块 每个模块进行单独打包

springboot项目目录结构 打包模块需要进行的配置 配置文件引入打包插件 <build><finalName>api</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifac…

APT80DQ40BG-ASEMI快恢复二极管APT80DQ40BG

编辑&#xff1a;ll APT80DQ40BG-ASEMI快恢复二极管APT80DQ40BG 型号&#xff1a;APT60DQ20BG 品牌&#xff1a;ASEMI 封装&#xff1a;TO-3P 恢复时间&#xff1a;≤50ns 正向电流&#xff1a;80A 反向耐压&#xff1a;400V 芯片个数&#xff1a;双芯片 引脚数量&…

MySQL|查看事务加锁情况

文章目录 使用information_schema数据库中的表获取锁信息INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS 使用SHOW ENGINE INNODB STATUS获取信息补充 使用information_schema数据库中的表获取锁信息 在information_schema数据库中&#xff0c;有几个与事务和锁紧密相关的表 INNOD…

回归预测 | MATLAB实现K折交叉验证GRNN广义回归神经网络多输入单输出回归预测

回归预测 | MATLAB实现K折交叉验证GRNN广义回归神经网络多输入单输出回归预测 目录 回归预测 | MATLAB实现K折交叉验证GRNN广义回归神经网络多输入单输出回归预测效果一览基本介绍研究内容程序设计参考资料效果一览 基本介绍 回归预测 | MATLAB实现K折交叉验证GRNN广义回归神经…

MySQL高阶知识点(一) 一条 SQL查询语句是如何被执行的

一条 SQL查询语句是如何被执行的 MySQL 的基本架构示意图如下所示&#xff1a;

MySQL分析查询语句Explain

1概述 ​ 定位了查询慢的SQL之后&#xff0c;就可以使用EXPLAIN或者DESCRIBE工具做针对性的分析查询。两者使用方法相同&#xff0c;并且分析结果也是相同的。 ​ MySQL中有专门负责SQL语句优化的优化器模块&#xff0c;主要功能是计算分析系统中收集到的统计信息&#xff0c…

生信豆芽菜-limma差异分析使用说明

网站&#xff1a;http://www.sxdyc.com/diffLimmaAnalyse 一、limma简介 什么是limma&#xff1f; 首先要明白&#xff0c;不管哪种差异分析&#xff0c;其本质都是广义线性模型。 limma也是广义线性模型的一种&#xff0c;其对每个gene的表达量拟合一个线性方程。 limma的分析…

模拟实现消息队列项目(系列8) -- 实现MqClient

目录 前言 1. 创建ConnectionFactory 2. Connection 和 Channel 的定义 2.1 Connection 2.2 Channel 3. 封装请求响应读写操作 3.1 写入请求 3.2 读取响应 3.3 Connection中创建Channel 4. 封装发送请求的操作 4.1 创建Channel 4.2 阻塞等待服务器响应 4.3 添加响应信息到 basi…

【构造】CF1798 D

Problem - D - Codeforces 题意&#xff1a; 思路&#xff1a; 首先如果 a 全是 00&#xff0c;那么显然无解。 否则考虑从左到右构造新数列&#xff0c;维护新数列的前缀和 s。 如果 s≥0&#xff0c;则在剩余未加入的数中随便选择一个非正数添加到新数列末尾。如果 s<…

Semantic Kernel 入门系列:Memory内存

了解的运作原理之后&#xff0c;就可以开始使用Semantic Kernel来制作应用了。 Semantic Kernel将embedding的功能封装到了Memory中&#xff0c;用来存储上下文信息&#xff0c;就好像电脑的内存一样&#xff0c;而LLM就像是CPU一样&#xff0c;我们所需要做的就是从内存中取出…