Vue源码系列讲解——虚拟DOM篇【三】(更新子节点)

1. 前言

在上一篇文章中,我们了解了Vue中的patch过程,即DOM-Diff算法。并且知道了在patch过程中基本会干三件事,分别是:创建节点,删除节点和更新节点。创建节点和删除节点都比较简单,而更新节点因为要处理各种可能出现的情况所以逻辑略微复杂一些,但是没关系,我们通过分析过程,对照源码,画逻辑流程图来帮助我们理解了其中的过程。最后我们还遗留了一个问题,那就是在更新节点过程中,新旧VNode可能都包含有子节点,对于子节点的对比更新会有额外的一些逻辑,那么在本篇文章中我们就来学习在Vue中是怎么对比更新子节点的。

2. 更新子节点

当新的VNode与旧的oldVNode都是元素节点并且都包含子节点时,那么这两个节点的VNode实例上的children属性就是所包含的子节点数组。我们把新的VNode上的子节点数组记为newChildren,把旧的oldVNode上的子节点数组记为oldChildren,我们把newChildren里面的元素与oldChildren里的元素一一进行对比,对比两个子节点数组肯定是要通过循环,外层循环newChildren数组,内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点,伪代码如下:

for (let i = 0; i < newChildren.length; i++) {const newChild = newChildren[i];for (let j = 0; j < oldChildren.length; j++) {const oldChild = oldChildren[j];if (newChild === oldChild) {// ...}}
}

那么以上这个过程将会存在以下四种情况:

  • 创建子节点

    如果newChildren里面的某个子节点在oldChildren里找不到与之相同的子节点,那么说明newChildren里面的这个子节点是之前没有的,是需要此次新增的节点,那么就创建子节点。

  • 删除子节点

    如果把newChildren里面的每一个子节点都循环完毕后,发现在oldChildren还有未处理的子节点,那就说明这些未处理的子节点是需要被废弃的,那么就将这些节点删除。

  • 移动子节点

    如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,这说明此次变化需要调整该子节点的位置,那就以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。

  • 更新节点

    如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。

OK,到这里,逻辑就相对清晰了,接下来我们只需分门别类的处理这四种情况就好了。

3. 创建子节点

如果newChildren里面的某个子节点在oldChildren里找不到与之相同的子节点,那么说明newChildren里面的这个子节点是之前没有的,是需要此次新增的节点,那么我们就创建这个节点,创建好之后再把它插入到DOM中合适的位置。

创建节点这个很容易,我们在上一篇文章的第三章已经介绍过了,这里就不再赘述了。

那么创建好之后如何插入到DOM中的合适的位置呢?显然,把节点插入到DOM中是很容易的,找到合适的位置是关键。接下来我们分析一下如何找这个合适的位置。我们看下面这个图: 

上图中左边是新的VNode,右边是旧的oldVNode,同时也是真实的DOM。这个图意思是当我们循环newChildren数组里面的子节点,前两个子节点都在oldChildren里找到了与之对应的子节点,那么我们将其处理,处理过后把它们标志为已处理,当循环到newChildren数组里第三个子节点时,发现在oldChildren里找不到与之对应的子节点,那么我们就需要创建这个节点,创建好之后我们发现这个节点本是newChildren数组里左起第三个子节点,那么我们就把创建好的节点插入到真实DOM里的第三个节点位置,也就是所有已处理节点之后,OK,此时我们拍手称快,所有已处理节点之后就是我们要找的合适的位置,但是真的是这样吗?我们再来看下面这个图: 

假如我们按照上面的方法把第三个节点插入到所有已处理节点之后,此时如果第四个节点也在oldChildren里找不到与之对应的节点,也是需要创建的节点,那么当我们把第四个节点也按照上面的说的插入到已处理节点之后,发现怎么插入到第三个位置了,可明明这个节点在newChildren数组里是第四个啊!

这就是问题所在,其实,我们应该把新创建的节点插入到所有未处理节点之前,这样以来逻辑才正确。后面不管有多少个新增的节点,每一个都插入到所有未处理节点之前,位置才不会错。

所以,合适的位置是所有未处理节点之前,而并非所有已处理节点之后

4. 删除子节点

如果把newChildren里面的每一个子节点都循环一遍,能在oldChildren数组里找到的就处理它,找不到的就新增,直到把newChildren里面所有子节点都过一遍后,发现在oldChildren还存在未处理的子节点,那就说明这些未处理的子节点是需要被废弃的,那么就将这些节点删除。

删除节点这个也很容易,我们在上一篇文章的第四章已经介绍过了,这里就不再赘述了。

5. 更新子节点

如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。

关于更新节点,我们在上一篇文章的第五章已经介绍过了,这里就不再赘述了。

6. 移动子节点

如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,这说明此次变化需要调整该子节点的位置,那就以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。

同样,移动一个节点不难,关键在于该移动到哪,或者说关键在于移动到哪个位置,这个位置才是关键。我们看下图: 

在上图中,绿色的两个节点是相同节点但是所处位置不同,即newChildren里面的第三个子节点与真实DOMoldChildren里面的第四个子节点相同但是所处位置不同,按照上面所说的,我们应该以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,所以我们应该把真实DOMoldChildren里面的第四个节点移动到第三个节点的位置,通过上图中的标注我们不难发现,所有未处理节点之前就是我们要移动的目的位置。如果此时你说那可不可以移动到所有已处理节点之后呢?那就又回到了更新节点时所遇到的那个问题了:如果前面有新增的节点呢?

7. 回到源码

OK,以上就是更新子节点时所要考虑的所有情况了,分析完以后,我们回到源码里看看实际情况是不是我们分析的这样子的,源码如下:

// 源码位置: /src/core/vdom/patch.jsif (isUndef(idxInOld)) {    // 如果在oldChildren里找不到当前循环的newChildren里的子节点// 新增节点并插入到合适位置createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {// 如果在oldChildren里找到了当前循环的newChildren里的子节点vnodeToMove = oldCh[idxInOld]// 如果两个节点相同if (sameVnode(vnodeToMove, newStartVnode)) {// 调用patchVnode更新节点patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefined// canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)}
}

以上代码中,首先判断在oldChildren里能否找到当前循环的newChildren里的子节点,如果找不到,那就是新增节点并插入到合适位置;如果找到了,先对比两个节点是否相同,若相同则先调用patchVnode更新节点,更新完之后再看是否需要移动节点,注意,源码里在判断是否需要移动子节点时用了简写的方式,下面这两种写法是等价的:

canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 等同于
if(canMove){nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}

我们看到,源码里的实现跟我们分析的是一样一样的。

8. 总结

本篇文章我们分析了Vue在更新子节点时是外层循环newChildren数组,内层循环oldChildren数组,把newChildren数组里的每一个元素分别与oldChildren数组里的每一个元素匹配,根据不同情况作出创建子节点、删除子节点、更新子节点以及移动子节点的操作。并且我们对不同情况的不同操作都进行了深入分析,分析之后我们回到源码验证我们分析的正确性,发现我们的分析跟源码的实现是一致的。

最后,我们再思考一个问题:这样双层循环虽然能解决问题,但是如果节点数量很多,这样循环算法的时间复杂度会不会很高?有没有什么可以优化的办法?答案当然是有的,并且Vue也意识到了这点,也进行了优化,那么下篇文章我们就来分析当节点数量很多时Vue是怎么优化算法的。

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

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

相关文章

12个最常用的matplotlib图例 !!

文章目录 1、折线图 2、散点图 3、直方图 4、柱状图 5、箱线图 6、热力图 7、饼图 8、面积图 9、等高线图 10、3D图 11、时间序列图 12、树状图 总结 1、折线图 折线图&#xff08;Line Plot&#xff09;&#xff1a;用于显示数据随时间或其他连续变量的变化趋势。在实际项目中…

【Linux驱动】块设备驱动(三)—— 块设备读写(不使用请求队列)

并非每种块设备都会用到请求队列&#xff0c;从上节可以知道&#xff0c;请求队列的作用是管理和调用IO请求&#xff0c;那么反过来想&#xff0c;如果IO请求较少&#xff0c;那就可以无需使用请求队列。在以下情况中&#xff0c;可以不使用请求队列。 单任务环境: 当系统中只有…

VRRP配置

目录 网络拓扑图 配置要求 配置步骤 网络拓扑图 配置要求 按照图示配置 IP 地址和网关在 SW1&#xff0c;SW2&#xff0c;SW3 上创建 Vlan10 和 Vlan20&#xff0c;对应 IP 网段如图&#xff0c;交换机之间链路允许所有 VLAN 通过在 SW1 和 SW2 上配置 VRRP&#xff0c;要求…

在windows server2016部署域控服务器DC

1.正常配置vmware虚拟机基础环境 2.启动虚拟机&#xff0c;会先到efi network&#xff0c;等待几分钟 3.进入boot manager&#xff0c;选择启动方式&#xff0c;记得提示CD启动的时候需要按回车&#xff0c;不然又会回到这个界面 4.选择安装版本为桌面版&#xff08;开始直接…

无题2024

念旧 阿悠悠 专辑&#xff1a;念旧 发行时间 2019-08-25 念旧 播报编辑讨论1上传视频 阿悠悠演唱歌曲 《念旧》是由一博作词&#xff0c;一博和张池作曲&#xff0c;阿悠悠演唱的歌曲&#xff0c;发行于2019年8月25日。 [1]收录于同名专辑《念旧》中。 相关星图 查…

(四)elasticsearch 源码之索引流程分析

https://www.cnblogs.com/darcy-yuan/p/17024341.html 1.概览 前面我们讨论了es是如何启动&#xff0c;本文研究下es是如何索引文档的。 下面是启动流程图&#xff0c;我们按照流程图的顺序依次描述。 其中主要类的关系如下: 2. 索引流程 (primary) 我们用postman发送请求&…

Windows Server 2025 Hyper-V 新变化

今天简单跟大家聊聊Windows Server 2025 Hyper-V一些新功能新变化&#xff0c;具体如下&#xff1a; 在 VM 之间共享 GPU 随着图形处理器的重要性日益增加&#xff0c;特别是由于它们在 AI 应用程序中的核心作用&#xff0c;Hyper-V 中对 GPU 的现有支持已不再足够。到目前为…

[Java][算法 哈希]Day 01---LeetCode 热题 100---01~03

LeetCode 热题 100---01~03 ------->哈希 第一题 两数之和 思路 最直接的理解就是 找出两个数的和等于目标数 这两个数可以相同 但是不能是同一个数字&#xff08;从数组上理解就是内存上不是同一位置&#xff09; 解法一&#xff1a;暴力法 暴力解万物 按照需求 …

报错ValueError: Unknown CUDA arch (8.6) or GPU not supported

文章目录 问题描述解决方案参考文献 问题描述 报错 ValueError: Unknown CUDA arch (8.6) or GPU not supported 本人显卡为 RTX 3060&#xff0c;CUDA 为 10.2&#xff0c;PyTorch 为 1.5 解决方案 修改 C:\Users\Administrator\Envs\test\Lib\site-packages\torch\utils\c…

【PyQt】08 - 编辑Tab顺序

文章目录 前言一、Tab顺序二、编辑Tab顺序总结 前言 介绍了什么是Tab顺序&#xff0c;以及如何修改Tab顺序。 一、Tab顺序 当你的界面设计好之后&#xff0c;在输入栏按住Tab按键&#xff0c;他会按照你摆放的顺序一次转跳 二、编辑Tab顺序 方法一 然后鼠标左击就可以改变…

前端JavaScript篇之对this对象的理解

目录 对this对象的理解1. 函数调用模式&#xff1a;2. 方法调用模式&#xff1a;3. 构造器调用模式&#xff1a;4. apply、call和bind调用模式&#xff1a; 对this对象的理解 在JavaScript中&#xff0c;this关键字是一个非常重要的概念&#xff0c;它用于指向当前执行上下文中…

【CV论文精读】EarlyBird: Early-Fusion for Multi-View Tracking in the Bird’s Eye View

【CV论文精读】EarlyBird: Early-Fusion for Multi-View Tracking in the Bird’s Eye View 0.论文摘要 多视图聚合有望克服多目标检测和跟踪中的遮挡和漏检挑战。多视图检测和3D对象检测中的最新方法通过将所有视图投影到地平面并在鸟瞰视图&#xff08;BEV&#xff09;中执…

面试经典150题 -- 栈(总结)

总的链接 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台 关于栈 -- stack 的学习链接 c的STL中的栈 -- stack-CSDN博客 20 . 有效的括号 这题直接用栈模拟就好了; 这里用一种取巧的方法 , 当遇见左括号&#xff0c;加入右…

JAVA设计模式之建造者模式详解

建造者模式 1 建造者模式介绍 建造者模式 (builder pattern), 也被称为生成器模式 , 是一种创建型设计模式. 定义: 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 **建造者模式要解决的问题 ** 建造者模式可以将部件和其组装过程分开…

Dynamo批量处理多个Revit文件?

Hello大家好&#xff01;我是九哥~ 最近很多小伙伴都在咨询Dynamo如何批量处理多个Revit文件&#xff0c;之前写过一篇《Dynamo批量修改多文件项目基点参数》&#xff0c;利用的是后台打开Revit的方式&#xff0c;可以实现一些批量操作的功能。 但是这个方法&#xff0c;对于一…

物理信息神经网络(PINN): 将物理知识融合到深度学习中

物理信息神经网络&#xff08;PINN&#xff09;: 将物理知识融合到深度学习中 物理信息神经网络&#xff08;PINN&#xff09;简介PINN的工作原理PINN模型如何利用物理法则指导模型训练1. 定义物理问题和相应的物理定律2. 构建神经网络3. 定义损失函数数据误差项 (Data-fidelit…

IoC原理

Spring框架的IOC是基于Java反射机制实现的&#xff0c;那具体怎么实现的&#xff0c;下面研究一下 反射 Java反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意方法…

多 split 窗口 in Gtkmm4

文章目录 效果预览实现概要源代码 效果预览 实现概要 使用Gtk::Paned虽然 Paned 只能装两个子控件, 但是我可以嵌套 paned1 装 box1 和 box2 paned2 装 paned1 和 box3 源代码 #include <gtkmm.h> class ExampleWindow : public Gtk::Window { public:ExampleWindow()…

大模型基础架构的变革:剖析Transformer的挑战者(下)

上一篇文章中&#xff0c;我们介绍了UniRepLKNet、StripedHyena、PanGu-π等有可能会替代Transformer的模型架构&#xff0c;这一篇文章我们将要介绍另外三个有可能会替代Transformer的模型架构&#xff0c;它们分别是StreamingLLM、SeTformer、Lightning Attention-2&#xff…

07 A B 从计数器到可控线性序列机

07. A.从计数器到可控线性序列机 让LED灯按照亮0.25秒。灭0.75秒的状态循环亮灭让LED灯按照亮0.25秒&#xff0c;灭0.5秒&#xff0c;亮0.75秒&#xff0c;灭1秒的状态循环亮灭让LED灯按照指定的亮灭模式亮灭&#xff0c;亮灭模式未知&#xff0c;由用户随即指定。以0.25秒为一…