虚拟DOM与Patching算法

虚拟DOM与Patching算法

⭐通过JS修改原生DOM的问题

我们使用原生js来修改DOM元素时,通常为以下步骤:

  1. 查询 DOM 元素:通过 JavaScript 代码,可以使用 DOM API(如 getElementByIdquerySelector 等方法)查询和获取需要修改的 DOM 元素。
  2. 修改 DOM 元素:一旦获取到 DOM 元素,可以通过 JavaScript 修改其属性、样式、内容等,以达到改变元素外观或行为的目的。
  3. 回流(重排):当修改了 DOM 元素后,浏览器需要重新计算并更新渲染树,这个过程称为回流(或者叫重排)。回流可能会影响其他元素的位置和大小,因此需要重新布局整个页面。
  4. 重绘:完成回流后,浏览器会对页面进行重绘,将新的渲染树绘制出来,使用户能够看到更新后的元素外观。
  5. 销毁:当不再需要某个 DOM 元素时,可以使用 JavaScript 代码移除它,从而销毁该元素以及其关联的事件处理程序等资源。

其中,回流和重绘是比较耗费资源的操作,如果我们进行大量DOM的销毁和生成,是会影响到整个页面的性能的。

那这和虚拟DOM有什么关系呢?DOM中的内容要动态修改,我们就要对DOM进行操作,这是无法避免的啊

这就要从我们的开发方式说起了

⭐为什么需要虚拟DOM

在以前,前端的项目还没有那么复杂,我们使用jQuery提供的方法就可以轻松进行DOM操作,哪里变了我就手动改哪里。

到后来,前端项目变得越来越复杂,交互增多,数据增加,页面元素也越来越多,渐渐地我们就有了力不从心的感觉,若有数十个,上百个地方的数据发生变动,我们都要手动进行DOM修改,这可是一项大工程。

这时,就轮到框架出场了,框架一出现,就告诉我们:你就负责管理数据,DOM的变动就交给我们吧!

从此以后,我们只需要动动小手,改一下数据,框架就会自动操作DOM,将更新呈现在页面上。

那么问题来了,框架是怎么知道我们的每个数据分别对应哪个DOM节点呢?

我们能想到几种不同的处理方式:

  1. 不管三七二十一,只要有内容发生变化,我们就将所有DOM节点都更新一遍。结合我们最开始所说的,DOM节点的修改会触发回流和重绘,这个操作很消耗性能,现在我们因为一处变动,就将所有DOM节点都更新一遍,显然是效率低的。
  2. 遍历一遍所有节点,挨个判断节点上引用的数据是不是我们发生变动的数据,如果是,就标记下来,等遍历完所有节点后,统一将被标记的节点进行更新,这就是Angular中采取的脏检查,脏检查的实现机制使得 Angular 能够自动追踪数据和视图之间的变化,从而保持它们的同步。不过,脏检查需要遍历所有注册的组件并比较其属性值,因此在性能方面可能会带来一定的开销。为了提高性能,Angular 还提供了其他的变化检测策略,如基于变更跟踪的检测策略和基于事件的检测策略,以满足不同场景下的需求。
  3. 第三种方法,我们将所有DOM节点抽象为一个模型,就想象成为一个人做一个手办,手办就是这个人的抽象模型,上面雕刻了这个人的部分特征,DOM节点的抽象过程也是如此,而我们将为整个DOM树打造的模型就叫做虚拟DOM,虚拟DOM上保留了真实DOM的部分特征,其中就包括DOM中节点引用的数据,每当数据发生改变,我们就去比对一下虚拟DOM中的各个节点,看看是哪个节点发生了变化,我们就去更新哪个真实节点,这就是React采取的更新方式。
  4. 第四种是借用虚拟DOM做的改进,虽然虚拟DOM比真实DOM重量更"轻",但不必要的比较总归是耗费性能的,正巧,vue中采用了变换侦测的方式来监听响应式数据,如果你了解其中原理,你就明白了,我们可以借用其中的watcher来快速定位到是哪一个组件发生了变化,在进行虚拟DOM比对的时候,我们只需要比对该组件中的节点即可,省时又省力。

一句话概括虚拟DOM的作用:虚拟 DOM 在前端开发中起到了桥梁的作用,它通过在内存中进行操作和比对,最终通过渲染引擎将变化更新到实际的 DOM 中,以提高性能、简化开发和实现跨平台支持等目标。

上述提到的数据就是我们常说的状态

⭐如何生成虚拟DOM

虚拟DOM指我们的虚拟DOM树,而树上会有很多节点,在vue中,虚拟DOM的节点被称作VNode,vue在最初渲染视图的时候,就会先创建一个vnode,而后使用vnode来生成真实的DOM元素,最终插入到页面中渲染视图。

由于每次渲染视图时,第一步就是创建vnode,所以我们可以每次将vnode做一份备份,将其缓存起来,之后要重新渲染视图时,就将进的vnode和旧的vnode进行一个比对,找出不一致的地方再对真实DOM进行修改

以下是VNode简化版的代码

js 复制代码
class VNode {
  constructor(tagName, attributes, children) {
    this.tagName = tagName; // 标签名
    this.attributes = attributes; // 属性对象
    this.children = children; // 子节点数组
  }

  render() {
    const element = document.createElement(this.tagName); // 创建对应的 DOM 元素

    // 设置属性
    for (const key in this.attributes) {
      element.setAttribute(key, this.attributes[key]);
    }

    // 渲染子节点
    for (const child of this.children) {
      if (child instanceof VNode) {
        // 递归渲染子节点
        element.appendChild(child.render());
      } else {
        // 文本节点
        element.appendChild(document.createTextNode(child));
      }
    }

    return element;
  }
}

真实的VNode类中还会有更多的属性,而VNode也有多种类型,不同类型都是由VNode类实例化出来的,唯一的区别只是其上的属性不同

我们现在已经可以创建出VNode了,下一步就是将它渲染到我们的页面上,覆盖真实DOM节点,实现数据更新,这里就要补充一个核心算法:patching算法

⭐创建&更新

在这一节中,我们将通过patching算法(也可称作diff算法),来进行VNode的比对以及真实DOM节点的更新

过程很好理解,我们直接上图(图来自《深入浅出Vue.js》------刘博文)

创建流程:

更新流程:

🌙子节点更新

子节点更新是虚拟DOM中必不可少的一部分,因为它能够对整个DOM树进行高效、准确、快速的渲染。

当进行子节点更新时,通常会遍历新旧节点树的所有子节点,并递归比较它们之间的差异。下面详细介绍子节点更新的具体策略:

  1. 子节点个数变化: 如果新旧节点的子节点个数不一致,就需要添加或删除相应的子节点来保持一致。

    在添加子节点时,需要先检查旧节点是否有子节点。如果没有,则直接将新节点添加到DOM中;否则,需要在旧节点的子节点列表中查找插入位置,以保证添加顺序正确。

    在删除子节点时,同样需要检查旧节点是否有子节点。如果没有,则无需进行任何操作;否则,需要在旧节点的子节点列表中查找要删除的节点,并将其从DOM中删除。

  2. 子节点顺序变化: 如果新旧节点的子节点顺序发生了变化,需要移动DOM中对应子节点的位置,以保持一致。

    在移动子节点时,可以使用DOM操作中的 insertBefore() 或 appendChild() 方法。这些方法能够根据给定的位置,在DOM中相应地插入节点。如果存在多个需要移动的节点,可以借鉴排序算法,比如快速排序或归并排序等,来保证节点的正确位置。

  3. 子节点差异比对: 对于相同位置的子节点,需要递归进行差异比对,以确定节点是否发生了变化。如果一个节点在新旧节点中都存在,但是内容或属性发生了变化,就需要对DOM进行更新。

    在进行递归比对时,需要注意以下几点:

    • 如果两个节点的类型不一致,则需要将其替换为新节点。
    • 如果两个节点都是文本节点,并且文本内容不一致,则需要更新该节点的文本内容。
    • 如果两个节点都是元素节点,并且它们的标签名和属性都相同,则需要递归比对它们的子节点。
    • 如果存在多组需要比对的节点,可以使用 diff queue 算法,将需要比对的节点加入队列中,避免递归过程中占用太多内存和时间。

以上是子节点更新的基本策略,实际应用中可能会有其他特殊情况和细节处理。虚拟DOM库和框架会根据自身的实现方式和优化策略,对子节点更新算法进行具体的实现和改进,例如在vue中,设计了比对优化策略,分别会对比新子节点中未处理节点的首位与旧子节点中未处理节点的首位新子节点中未处理节点的末位与旧子节点中未处理节点的末位新子节点中未处理节点的首位和旧子节点中未处理节点的末位新子节点中未处理节点的末位与旧子节点中未处理节点的首位,注意这里强调了未被处理,处理过的子节点则是已经完成增删改移操作的节点,处理过的节点就可以跳过不处理

由于我们会首位互相进行交叉对比,所以已处理节点会在首位两端出现,所以vue就使用头尾指针分别指向未被处理节点的方式进行循环遍历,直到前后指针交叉,证明遍历结束。

PS:在对比新旧子节点时,我们设置的key就派上了用场,可以直接通过key来获取index,就不需要在旧子节点中循环遍历寻找相同节点

如此流程进行下来,旧节点就被替换为了新节点,生成真实DOM,渲染到页面上,完成dom更新。

相关推荐
虾球xz9 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇14 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒18 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员34 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐36 分钟前
前端图像处理(一)
前端
程序猿阿伟43 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express