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://xiahunao.cn/news/2776651.html

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

相关文章

“掌握温度,感知湿度,一触即知!”DHT11温湿度传感器,为您的生活增添一份关怀与精准。#非标协议【下】

“掌握温度&#xff0c;感知湿度&#xff0c;一触即知&#xff01;”DHT11温湿度传感器&#xff0c;为您的生活增添一份关怀与精准。#非标协议【下】 前言预备知识1.DHT11温湿度传感器初识1.1产品概述1.2与51单片机接线1.3数据传送逻辑和数据格式 2.发送时序检测DHT11温湿度传感…

npm 上传一个自己的应用(3) 在项目中导入及使用自己上传到NPM的工具

上文 npm 上传一个自己的应用(2) 创建一个JavaScript函数 并发布到NPM 我们创建了一个函数 并发上了npm 最后 我们这里 我们可以看到它的安装指令 这里 我们可以打开一个vue项目 终端输入 我们的安装指令 npm i 自己的包 如下代码 npm i grtest我们在 node_modules目录 下…

[C#]winform制作仪表盘好用的表盘控件和使用方法

【仪表盘一般创建流程】 在C#中制作仪表盘文案&#xff08;通常指仪表盘上的文本、数字或指标显示&#xff09;涉及到使用图形用户界面&#xff08;GUI&#xff09;组件&#xff0c;比如Windows Forms、WPF (Windows Presentation Foundation) 或 ASP.NET 等。以下是一个使用W…

Linux网络通信——TCP/OSI七层模型/TCP/IP(五层或四层模型)/HTTP报文传输原理

文章目录 消息的传输什么是OSI七层模型OSI七层模型的内容物理层&#xff08;Physical Layer&#xff09;&#xff1a;数据链路层&#xff08;Data Link Layer&#xff09;&#xff1a;网络层&#xff08;Network Layer&#xff09;&#xff1a;传输层&#xff08;Transport Lay…

vue3-内置组件-Teleport

Teleport <Teleport> 是一个内置组件&#xff0c;它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。 基本用法 有时我们可能会遇到这样的场景&#xff1a;一个组件模板的一部分在逻辑上从属于该组件&#xff0c;但从整个应用视图的角度来看…

实现注册登录时数据的加密传输(含前后端具体代码)

前言 http/https协议提交在被抓包时请求内容是明文的, 直接传输账号密码的风险非常大&#xff0c;故这里我们要对数据加密处理&#xff0c;并生成校验码&#xff0c;防止数据篡改 目录 ​编辑 前言 具体思路 代码实现 前端信息加密处理&#xff08;Vue&#xff09; 安装…

Java多线程:线程安全

&#x1f451;专栏内容&#xff1a;Java⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、线程状态1、New&#xff08;初始状态&#xff09;2、Terminated&#xff08;终止状态&#xff09;3、Runnable&#xff08;…

C++类型转化cast from pointer to smaller type ‘int‘ loses information

代码如下 #include <iostream>int main() {int a 10;std::cout << (int)&a << std::endl;return 0; }编译 这段代码是要将地址转化成整数类型&#xff0c;但是在编译时编译器告诉我们这是错的&#xff0c;因为在C中&#xff0c;将指针转换为int类型的…

Spring基础 - Spring核心之面向切面编程(AOP)

Spring基础 - Spring核心之面向切面编程(AOP) 引入 Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦&#xff0c;这个就叫面向切面编程 - Aspect Oriented Programming (AOP)那么Spring框架又是如何实现AOP的呢&#xff1f; 这就引入代理技术&#xff0c;分静…

Sqlite3安装步骤

1、Sqlite3以下载文件&#xff0c;配置环境变量的方式进行安装。 2、下方链接为官方的下载地址。 sqlite下载地址 2.1、需要两个下载文件&#xff0c;解压后将他们放在一起&#xff0c;假设解压后的路径为E:\sqlite。 sqlite-dll-win-x64-3450100.zip sqlite-tools-win-x6…

C++自定义函数详解

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 铁汁们新年好呀&#xff0c;今天我们来了解自定义函数。 文章目录 1.数学中的函数 2.什么是自定义函数 3.自定义函数如何使用&#xff1f; 4.值传递和引用传递&#xff08;形参和实参区分&#xff09; …

OLED调试简介

文章目录 一、介绍调试方法介绍OLED简介硬件电路OLED驱动函数 二、操作连接线路使用驱动函数显示内容 OLED.c的内容 一、介绍 调试方法介绍 OLED简介 硬件电路 OLED驱动函数 二、操作 连接线路 因为这两个引脚不做配置是浮空状态&#xff0c;在这里直接用电源给OLED供电 使…

嵌入式学习之Linux入门篇笔记——10,Linux连接档概念

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 目录 1.Linux 下的连接档种类 2.什么是 inode&#xff1f; 3.什…

Node.js JSON Schema Ajv依赖库逐步介绍验证类型和中文错误提示

在构建应用程序时&#xff0c;数据的有效性是至关重要的。为了确保传入的数据符合预期的格式和规范&#xff0c;我们可以使用 Ajv&#xff08;Another JSON Schema Validator&#xff09;进行验证。在这篇博文中&#xff0c;我们将从头开始学习 Ajv&#xff0c;逐步介绍验证类型…

Linux探秘之旅:透彻理解路径、命令与系统概念

目录 如何远程连接 远程登录简明指南 linux区别 1.严格区分大小写 2.linux的命令返回结果判断 3.如何查看网络信息 4.关于后缀名&#xff08;Linux不关心文件后缀&#xff09; 4.1 需要记忆的后缀 5.echo命令 6.linux一切皆文件 6.1比如磁盘的文件 6.2可执行文件 …

在面试中如何回复擅长vue还是react

当面试官问及这个问题的时候&#xff0c;我们需要思考面试官是否是在乎你是掌握vue还是react吗&#xff1f;&#xff1f;&#xff1f; 在大前端的一个环境下&#xff0c;当前又有AI人工智能的加持辅助&#xff0c;我们是不是要去思考企业在进行前端岗位人员需求的时候&#xf…

【原创】Qt库open62541 MinGW编译

一、前言 为了统一公司的驱动层开发&#xff0c;准备采用OpcUA的方式转发底层数据&#xff0c;而服务器有Windows Server&#xff0c;也有CentOS&#xff0c;因此想用Qt开发一个基于MinGW的OpcUA Server&#xff0c;这样就能跨平台部署。这里记录一下&#xff0c;希望对你也有用…

Android Studio无法安装Git问题解决(折中方案)

安装配置好studio&#xff0c;往往会使用git克隆github上面的项目&#xff0c;但是却发现git无法正确安装&#xff0c;本文将介绍如何解决git无法安装这一问题。 对于git安装&#xff0c;实际比较复杂&#xff0c;可以参考这一篇博客。 Git 详细安装教程&#xff08;详解 Gi…

Java多线程:`Thread`类

&#x1f451;专栏内容&#xff1a;Java⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、Thread的常见构造方法二、Thread 的常见属性三、Thread的常用方法1、start方法2、中断一个线程Ⅰ、通过共享标记Ⅱ、调用in…

Git版本与分支

目录 一、Git 二、配置SSH 1.什么是SSH Key 2.配置SSH Key 三、分支 1.为什么要使用分支 2.四个环境及特点 3.实践操作 1.创建分支 2.查看分支 3.切换分支 4.合并分支 5.删除分支 6.重命名分支 7.推送远程分支 8.拉取远程分支 9.克隆指定分支 四、版本 1.什…