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

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)

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

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

相关文章

无需公网-用zerotier异地组网

无需公网-用zerotier异地组网 在前面的文章中我们讲到利用frp进行内网穿透&#xff0c;但是他的局限在于你需要一台公网服务器。并且对公网服务器的带宽有一定的要求。因此这里我们推荐一款异地组网工具搭建属于自己的虚拟网络&#xff0c;经过授权连接成功之后彼此都在同一网…

Oracle单实例升级补丁

目录 1.当前DB环境2.下载补丁包和opatch的升级包3.检查OPatch的版本4.检查补丁是否冲突5.关闭数据库实例&#xff0c;关闭监听6.应用patch7.加载变化的SQL到数据库8.ORACLE升级补丁查询 oracle19.3升级补丁到19.18 1.当前DB环境 [oraclelocalhost ~]$ cat /etc/redhat-releas…

\vendor\github.com\godror\orahlp.go:531:19: undefined: VersionInfo

…\goAdmin\vendor\github.com\godror\orahlp.go:531:19: undefined: VersionInfo 解决办法 降了go版本(go1.18)&#xff0c;之前是go1.19 gorm版本不能用最新的&#xff0c;降至&#xff08;gorm.io/gorm v1.21.16&#xff09;就可以 修改交插编译参数 go env -w CGO_ENABLED1…

# ⛳ Docker 安装、配置和详细使用教程-Win10专业版

目录 ⛳ Docker 安装、配置和详细使用教程-Win10专业版&#x1f69c; 一、win10 系统配置&#x1f3a8; 二、Docker下载和安装&#x1f3ed; 三、Docker配置&#x1f389; 四、Docker入门使用 ⛳ Docker 安装、配置和详细使用教程-Win10专业版 &#x1f69c; 一、win10 系统配…

区块链实验室(15) - 编译FISCO BCOS的过程监测

首次编译开源项目&#xff0c;一般需要下载很多依赖包&#xff0c;尤其是从github、sourceforge等下载依赖包时&#xff0c;速度很慢&#xff0c;编译进度似乎没有一点反应&#xff0c;似乎陷入死循环&#xff0c;似乎陷入一个没有结果的等待。本文提供一种监测方法&#xff0c…

redis的事务和watch机制

这里写目录标题 第一章、redis事务和watch机制1.1&#xff09;redis事务&#xff0c;事务的三大命令语法&#xff1a;开启事务 multi语法&#xff1a;执行事务 exec语法&#xff1a;取消事务 discard 1.2&#xff09;redis事务的错误和回滚的情况1.3&#xff09;watch机制语法&…

【Linux】为.sh脚本制作桌面快捷方式(.desktop,可双击执行),且替换显示图标(图文详情)

目录 0.背景环境 1、原理 2、详细步骤 1&#xff09;创建.desktop快捷方式 2&#xff09; 给test.desktop快捷方式增加可执行权限 3&#xff09;编辑test.desktop内容和参数 4&#xff09;修改快捷方式属性为双击可执行 5&#xff09;将桌面快捷方式发送到桌面 0.背景环…

2023全新UI好看的社区源码下载/反编译版

2023全新UI好看的社区源码下载/反编译版 这次分享一个RuleAPP二开美化版&#xff08;尊重每个作者版权&#xff09;&#xff0c;无加密可反编译版本放压缩包了&#xff0c;自己弄吧&#xff01;&#xff01;&#xff01; RuleAPP本身就是一款免费开源强大的社区&#xff0c;基…

交替方向乘子

目录 一&#xff0c;交替方向乘子ADMM 1&#xff0c;带线性约束的分离优化模型 2&#xff0c;常见优化模型转带线性约束的分离优化模型 3&#xff0c;带线性约束的分离优化模型求解 4&#xff0c;交替方向乘子ADMM 本文部分内容来自教材 一&#xff0c;交替方向乘子ADMM …

Linux计划任务管理at、crond

一、单次任务at at命令可以设置在一个指定的时间执行一个指定任务&#xff0c;只能执行一次&#xff0c;使用前确认系统开启了atd服务。 例如&#xff1a;定时执行某命令或脚本&#xff0c; 1、输入at 19:00&#xff0c;回车&#xff1b; 2、输入需要执行的命令或脚本文件&am…

如何给Google Chrome增加proxy

1. 先打开https://github.com/KaranGauswami/socks-to-http-proxy/releases 我的电脑是Liunx系统所以下载第一个 2. 下载完之后把这个文件变成可执行文件&#xff0c;可以是用这个命令 chmod x 文件名 3. 然后执行这个命令&#xff1a; ./sthp-linux -p 8080 -s 127.0.0.1:…

c#设计模式-创建型模式 之 工厂模式

前言&#xff1a; 工厂模式&#xff08;Factory Pattern&#xff09;是一种常用的对象创建型设计模式。该模式的主要思想是提供一个创建对象的接口&#xff08;也可以是抽象类、静态方法等&#xff09;&#xff0c;将实际创建对象的工作推迟到子类中进行。这样一来&#xff0c…

APT80DQ40BG-ASEMI快恢复二极管APT80DQ40BG

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

SpringBoot之Actuator基本使用

SpringBoot之Actuator基本使用 引入分类常用接口含义healthbeansconditionsheapdumpmappingsthreaddumploggersmetrics 引入 <!-- actuator start--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter…

基于Azure OpenAI Service 的知识库搭建实验⼿册

1.概要 介绍如何使⽤Azure OpenAI Service 的嵌⼊技术&#xff0c;创建知识库&#xff1b;以及创建必要的资源组和资源&#xff0c;包括 Form Recognizer 资源和 Azure 翻译器资源。在创建问答机器⼈服务时&#xff0c;需要使⽤已部署模型的 Azure OpenAI 资源、已存在的…

Openlayers实战:使几何图形适配窗口

Openlayers开发的项目中,有一种应用非常重要,就是绘制或者显示出几何图形后,让几何图形居中并适配到窗口下,这样能让用户很好的聚焦到所要看的内容中去。 这里使用了fit的这个view 的方法,具体的操作请参考示例源代码。 效果图 源代码 /* * @Author: 大剑师兰特(xiaozh…

【C# 基础精讲】循环语句:for、while、do-while

循环语句是C#编程中用于重复执行一段代码块的关键结构。C#支持for、while和do-while三种常见的循环语句&#xff0c;它们允许根据条件来控制代码块的重复执行。在本文中&#xff0c;我们将详细介绍这三种循环语句的语法和使用方法。 for循环 for循环是一种常见的循环结构&…

每天一道leetcode:剑指 Offer 32 - III. 从上到下打印二叉树 III(中等广度优先遍历)

今日份题目&#xff1a; 请实现一个函数按照之字形顺序打印二叉树&#xff0c;即第一行按照从左到右的顺序打印&#xff0c;第二层按照从右到左的顺序打印&#xff0c;第三行再按照从左到右的顺序打印&#xff0c;其他行以此类推。 示例 给定二叉树: [3,9,20,null,null,15,7…

【云原生】Kubernetes节点亲和性分配 Pod

目录 1 给节点添加标签 2 根据选择节点标签指派 pod 到指定节点[nodeSelector] 3 根据节点名称指派 pod 到指定节点[nodeName] 4 根据 亲和性和反亲和性 指派 pod 到指定节点 5 节点亲和性权重 6 pod 间亲和性和反亲和性及权重 7 污点和容忍度 8 Pod 拓扑分布约束 官方…

python几岁可以学零基础,python多大的孩子可以学

大家好&#xff0c;小编为大家解答多大的孩子可以学python的问题。很多人还不知道学python多大年龄可以学&#xff0c;现在让我们一起来看看吧&#xff01; python编程是现在很多孩子接触编程的好选择&#xff0c;它能够给孩子带来容易入门的效果。那么&#xff0c;python编程少…