Vue2源码解析 patch(将vnode渲染成真实dom)

2023-10-17 10:59

本文主要是介绍Vue2源码解析 patch(将vnode渲染成真实dom),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1  patch介绍

1.1  新增节点

1.2  删除节点

1.3  更新节点

1.4  patch运行流程

 2  创建节点

 3  删除节点

 4  更新节点

4.1  静态节点

4.2  新虚拟节点有文本属性

4.3  新虚拟节点无文本属性

4.4  更新节点小结

 5  更新子节点

5.1  更新策略

5.2  优化策略

5.3  哪些节点是未处理过的

5.4  更新子节点小结

 6  总结


1  patch介绍

虚拟dom最核心的部分是patch,它可以将vnode渲染成真实的dom。

patch也可以叫作patching算法,通过它渲染真实dom时,对比新旧两个vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新。

patch是在现有dom上进行修改来达到渲染视图的目的,对现有dom进行修改需要做三件事:

        1.创建新增的节点;

        2.删除已经废弃的节点;

        3.修改需要更新的节点。

1.1  新增节点

为了考虑性能问题,所以vue用对比更新的方法。

新增节点的场景1:当oldVnode不存在而vnode存在时,就需要使用vnode生成真实dom元素并将其插入到视图中去,如下图所示。

 新增节点的场景2:当vnode和oldVnode完全不是同一个节点时(可得知vnode就是一个全新的节点,而oldVnode就是一个被废弃的节点),需要使用vnode生成真实的dom元素并将其插入到视图当中,如下图所示。

 使用vnode创建一个新dom节点,用它去替换oldVnode所对应的真实dom节点。

1.2  删除节点

删除节点的场景1:当一个节点只在oldVnode中存在时,需要把它从dom中删除。因为渲染视图时,需要以vnode为标准,所以vnode中不存在的节点都属于被废弃的节点,而被废弃的节点需要从dom中删除。

删除节点的场景2:当oldVnode和vnode完全不是同一个节点时,在dom中需要使用vnode创建的新节点替换oldVnode所对应的旧节点,而替换过程是新创建的dom节点插入到旧节点的旁边,然后再将旧节点删除。

1.3  更新节点

更新节点的场景1:新旧两个节点是同一个节点,需要把这两个节点进行细致的对比,然后对oldVnode在视图中所对应的真实节点进行更新,案例如下图所示。

1.4  patch运行流程

 2  创建节点

事实上,只有三种类型的节点会被创建并插入到dom中:元素节点、注释节点、文本节点

创建元素节点的过程:

        1.判断vnode是否是元素节点,只需要判断它是否具有tag属性即可。如果一个vnode具有tag属性,就认为是元素属性。

        2.调用当前环境下的createElement方法创建真实的元素节点

        3.调用appendChild将元素插入到指定的父节点中;

        4.元素节点通常会有子节点(children),所以当一个元素节点被创建后,需要将它的子节点也创建出来并插入到这个刚创建出的节点下面;

        5.创建子节点的过程是一个递归过程。vnode中的children属性保存了当前节点的所有子虚拟节点,所以只需要将vnode中的children属性循环一遍,将每个子虚拟节点都执行一遍创建元素的逻辑,就可以实现我们想要的功能。

将虚拟dom创建真实dom,最后渲染到视图的过程,如下图所示:

 一个元素节点从创建到渲染视图的过程,如下图所示:

 除了元素节点之外,要创建的还有注释节点和文本节点。

注释节点,vnode中不存在tag属性,isComment属性为true,用document.createComment创建并插入到指定的父节点中;

文本节点,vnode中不存在tag属性,isComment属性不为true,用docuemnt.createTextNode创建并插入到指定的父节点中。

创建节点并渲染到视图的过程如下图所示:

 3  删除节点

vue源码中删除元素的代码如下逻辑: 

function removeVnodes(vnodes,startIdx,endIdx){for(;startIdx <= endIdx;++startIdx){const ch = vnodes[startIdx]if(isDef(ch)){removeNode(ch.elm)}}
}

删除vnodes数组中从startIdx指定到endIdx指定位置的内容;removeNode用于删除视图中单个节点,而removeVnodes用于删除一组指定的节点。

removeNode的实现逻辑如下:

// 将当前元素从它的父节点中删除,其中nodeOps是对节点操作的封装
const nodeOps = {removeChild(node,child){node.removeChild(child)}
}
function removeNode(el){const parent = nodeOps.parentNode(el)if(isDef(parent)){nodeOps.removeNode(parent,el)}
}

为什么不直接使用parent.removeChild(child)删除节点,而是将这个节点操作封装成函数放在nodeOps里呢?

因为跨平台渲染时框架的渲染机制和dom解耦。只要把框架更新dom时的节点操作进行封装,就可以实现跨平台渲染,在不同平台下调用节点的操作。

 4  更新节点

4.1  静态节点

在更新节点时,首先需要判断新旧两个虚拟节点是否是静态节点,如果是,就不需要进行新操作,可以直接跳过更新节点的过程。

4.2  新虚拟节点有文本属性

当新虚拟节点有文本属性,并且和就虚拟节点的文本属性不一样时以新虚拟节点vnode为准来更新视图),我们可以直接把视图中的真实dom节点的内容修改成新虚拟节点的文本。

4.3  新虚拟节点无文本属性

如果新创建的虚拟节点没有text属性,那么它就是一个元素节点。元素节点通常会有子节点,也就是children属性,但是有可能没有子节点,所以得分情况处理:

1.有children的情况

当新创建的虚拟节点有children属性时,其实还会有两种情况,那就是看旧虚拟节点(oldVnode)是否有children属性。

如果旧虚拟节点也有children属性,那么对新旧两个虚拟节点的children进行一个更详细的对比并更新。

如果旧虚拟节点没有children属性,那么说明旧虚拟节点要么是一个空标签,要么是有文本的文本节点。如果是文本节点,那么先把文本清空让它变成空标签,然后将新虚拟节点(vnode)中的children挨个创建真实的dom元素节点并将其插入到视图中的dom节点下面。

2.无children的情况

当新创建的虚拟节点既没有text属性,也没有children属性时,说明这个新创建的节点是一个空节点,它下面既没有文本也没有子节点,这时如果旧虚拟节点中有子节点就删除子节点,有文本就删除文本。有什么删什么,最后达到视图中是空标签的目的。

4.4  更新节点小结

更新节点的逻辑如下图所示:

 在源码中,真实过程如下图所示:

 5  更新子节点

更新子节点大概可以分为4中操作:更新节点、新增节点、删除节点、移动节点位置。

例如,newChildren(新子节点列表)中有一个节点在oldChildren(旧子节点列表)中找不到相同的节点,这说明这个节点是因为状态更改而新增的节点,此时需要进行新增节点的操作。

例如,newChildren中的某个节点和oldChildren中的某个子节点是一个节点,但位置不同,这说明由于状态变化而位置发生了移动的节点,这时需要节点的移动操作。

对比两个子节点列表(children),首先需要做的事情是循环。循环newChildren(新子节点列表),每循环到一个新子节点,就去oldChildren(旧子节点列表)中找和当前节点相同的那个旧子节点。如果在oldChildren中找不到,说明当前子节点是由于状态变化而新增的节点,我们要进行创建节点并插入视图的操作;如果找到了,就做更新操作;如果找到的旧子节点的位置和新子节点不同,则需要移动节点等。

5.1  更新策略

1.创建子节点

新旧两个子节点列表是通过循环进行对比的,所以创建节点的操作是在循环体内执行的,其具体是在oldChildren(旧子节点列表)中寻找本次循环所指向的新子节点。

如果在oldChildren中没有找到与本次循环所指向的新子节点相同的节点,那么说明本次循环所指向的新节点是一个新增节点。对于新增节点,需要执行创建节点的操作,并将新创建的节点插入到oldChilren中所有未处理节点的前面。当节点成功插入dom后,这一轮循环旧结束了。

案例:下入中最上面是真实的dom节点,左下角的节点是新创建的虚拟节点,右下角的节点是虚拟节点。

上图中,表示已经对前两个子节点进行了更新,当前正在处理第三个子节点。当右下角的虚拟子节点中找不到与左下角的第三个节点相同的节点时,证明是新增节点,需要创建节点并插入到真实dom中,插入的位置是所有未处理节点的前面(如下图所示),也就是所指定的位置。

 为什么插入到所有已处理节点的后面不行吗?不行,如果这个新节点后面也是新节点那插入位置就不正确了。如下图所示:

 2.更新子节点

更新节点本质上当一个节点同时存在于newChildren和oldChildren中时需要执行的操作。

情况1:两个节点是同一个节点并且位置相同,只需要更新节点的操作即可。如下图所示:

情况2:oldChildren中子节点的位置和本次 循环所指向的新子节点的位置不一致时,除了对真实dom节点进行更新操作外,还需要对真实dom节点进行移动节点的操作。

3.移动子节点

移动子节点的场景1:发生在newChildren中的某个节点和oldChildren中的某个节点是同一个节点,但是位置不同,所以在真实的dom中需要将这个节点的位置以新虚拟节点的位置为准进行移动,如下图所示。

 通过Node.insertBefore(),可以成功地将一个已有节点移动到一个指定的位置。

但是,怎么知道应该把节点移动到哪里呢?

答:这个节点的位置是所有未处理节点的第一个节点。对比两个子节点列表是通过从左到右循环newChildren这个列表,然后每循环一个节点,就去oldChildren中寻找与这个节点相同的节点进行处理。也就是说,newChildren中当前被循环到的这个节点的左边都是被处理过的。如下图所示:

 4.删除子节点

删除子节点,本质上是删除那些oldChildren中存在但newChildren中不存在的节点

当newChildren中的所有节点都被循环了一遍后,也就是循环结束后,如果oldChildren中还有剩余的没有被处理的节点,那么这些节点就是被废弃、需要删除的节点。

5.2  优化策略

通常情况下,并不是所有子节点的位置都会发生移动,一个列表中总有几个节点的位置是不变的,我们需要有快捷的方式(非循环的方式)来找到这些节点。

快速的查找节点的方式称为快捷查询,共有4种方式,分别是:

        1.新前与旧后

        2.新后与旧后

        3.新后与旧前

        4.新前与旧后

新前:newChildren中所有未处理的第一个节点;

新后:newChildren中所有未处理的最后一个节点;

旧前:oldChildren中所有未处理的第一个节点;

旧后:oldChildren中所有未处理的最后一个节点。

 1.新前与旧前

由于新前和旧前的位置相同,属于同一个节点,所以并不需要执行移动节点的操作,只需要更新节点即可。

如果不是同一个节点,没关系,一共有4种快捷查询方式,挨个试一次。如果都不行,最后再使用循环来查询节点。

2.新后与旧后 

当新前和旧前对比不是同一个节点,尝试用新后与旧后对比是否是同一个节点。如果是同一个节点,就将这两个节点进行对比并更新视图,如下图所示;不是同一个节点继续尝试新后和旧前对比。

 3.新后与旧前

对比新后与旧前是否是同一个节点,如果是,对比并更新视图。如下图所示:

新后与旧前是同一个节点,但是位置不同,所以除了更新节点外,还需要执行移动节点的操作。如下图所示:

当新后与旧前是同一个节点时,在真实dom种除了做更新操作外,还需要将节点移动到oldChildren中所有未处理节点的最后面

为什么是移动到oldChildren中所有未处理节点的最后面?因为更新节点是以新虚拟节点为准,子节点也不例外。

 当真实dom子节点左右两侧已经有节点被更新,只有中间这部分节点未处理时,新后这个节点是未处理节点中的最后一个节点,所以真的dom节点移动位置时,需要移动到oldChildren中所有未处理节点的最后面。

 4.新前和旧后

新前和旧后进行对比是否是同一个节点,是则进行更新节点的操作。如下图所示:

 加入是同一个节点,但是位置不相同,除了更新节点外,还需要节点的移动操作,将节点移动到oldChildren中所有未处理节点的最前面。如下图所示:

将节点移动到oldChildren中所有未处理节点的最前面的原因:当真实的dom节点中已经有节点被更新,并且更新到第二个节点时,我们发现oldChildren中对应的节点在第三个位置上,这时需要将旧后这个节点更新并移动到第二个位置上,所以只需要将节点移动到所有未处理节点的最前面,就能实现移动到第二个位置的目的。如下图所示:

5.3  哪些节点是未处理过的

怎么分辨哪些节点是处理过的,哪些节点是未处理过的呢?
我们的逻辑都是在循环体内处理的,所有只要让循环条件保证只有未处理的节点才能进入循环体内,就能达到忽略已处理的节点从而对未处理节点进行对比和更新等操作。

但由于前面的优化策略,节点是有可能会从后面对比的,对比成功就会进行更新处理,也就是说,循环体内的逻辑由于优化策略,不再是只处理所有未处理的节点的第一个,而是有可能会处理最后一个,这种情况下不能从前往后循环,而是应该从两边向中间循环。

那么,怎样实现两边向中间循环呢?

首先,准备4个变量:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx;

在循环体内,每处理一个节点,就将下标向指定的方向移动一个位置,通常情况下是对新旧两个节点进行更新操作,就相当于一次性处理两个节点,将新旧两个节点的下标都向指定方向移动一个位置。

当开始位置大于等于结束位置时,说明所有节点都遍历过了,则结束循环:

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){// to do
}

通过上面的循环条件,就可以保证循环体内的节点都是未处理的。

5.4  更新子节点小结

更新子节点的整体流程如下图所示:

 

 6  总结

虚拟dom的最关键部分是:patch;

通过patch可以对比新旧虚拟dom,从而只针对发生了变化的节点进行更新视图的操作。

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

这篇关于Vue2源码解析 patch(将vnode渲染成真实dom)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/224929

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第二题解析 未完

一、题目阅读 (最大值之和)给定整数序列 a0,⋯,an−1,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤105 和 1≤ai≤108。 一个序列的非空连续子序列可以用两个下标 ll 和 rr(其中0≤l≤r<n0≤l≤r<n)表示,对应的序列为 al,al+1,⋯,ar​。两个非空连续子序列不同,当且仅当下标不同。 例如,当原序列为 [1,2,1,2] 时,要计算子序列 [