结论先行:
一、什么是虚拟 DOM ?
虚拟 DOM 是真实 DOM 的 JS 对象,指用 JS 对象描述 DOM 树结构,包括节点的类型、属性和子节点等信息。
它的优点呢,一个是跨平台,再一个就是支持 diff 算法。
当响应式数据发生变化的时候,Vue 会生成一个新的虚拟DOM,diff 算法比对新旧虚拟DOM,快速找到这两个 js 对象的差异,再去更新视图,减少对真实 DOM 的操作从而提高了性能。
虚拟 DOM 不会立即操作真实 DOM ,底层经过 diff 算法得出需要修改的最小单位,再去更新视图,减少对真实 DOM 的操作从而提高了性能。
① Vue 组件挂载时会调用 render 函数生成虚拟 DOM
② 而当响应式数据发生变化时,Vue 会重新创建一个新的虚拟 DOM- newVnode
③ 底层通过 diff 算法来比对新旧虚拟DOM,快速找到这两个 js 对象之前的差异,得出需要修改的最小单位,再去更新视图,减少对真实 DOM 的操作从而提高了性能和效率。
虚拟 DOM 抽象了原本的渲染过程,不依赖真实平台环境,从而实现了跨平台的能力。
二、diff 算法
diff 算法的核心就是比较新旧两个虚拟 DOM 树的差异,再去最小化的更新视图,从而提高渲染性能。
② 实现原理
diff 算法的实现原理是从根节点开始逐层遍历新旧两个虚拟 DOM 树,比较节点的类型、属性和子节点等内容。
如果节点有差异,则记录该差异,并将其添加到一个差异队列中。在比较完成后,根据差异类型执行响应的更新操作。比如替换节点、修改属性、移动节点位置等等。
③ Vue 的 diff 算法具体实现
首先对比新旧虚拟 DOM 树的根节点,如果不同,则直接替换整个DOM节点,并结束 diff 过程。
如果根节点相同,则深度遍历对比新旧虚拟 DOM 树的子节点列表。采用的是双指针和优化比较的策略,优先头头、尾尾、头尾、尾头比较,优化了我们经常使用的 DOM 操作。若是乱序的情况下,就为节点设置 key 创建映射表,然后通过对比查找的方式比对。
如果子节点有不同,则继续比较子节点,直到所有子节点比较完成。如果子节点也都相同,则不需要更新,直接退出比较。
首先比较新旧节点的标签名,如果不同则直接替换成新节点。
如果标签名相同,则比较节点的属性和事件,如果不同,直接修改成新节点。
如果节点相同,则比较节点的子节点,如果子节点有不同,则继续比较子节点,直到所有子节点比较完成。
如果子节点也相同,则不需要更新,直接退出比较。
比较子节点有以下4种情况:
- 如果都是文本节点且不相等,直接更新新节点的文本内容即可;
- 如果老的有子节点,新的没子节点,直接删除老的子节点;
- 如果老的没子节点,新的有子节点,那么就创造新的子节点,直接插入父节点中;
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点。其中,diff 算法的核心是两个都有子节点的情况,底层采用的是双指针和优化比较的策略,优先头头、尾尾、头尾、尾头比较,优化了我们经常使用的 DOM 操作。那若是乱序的情况下,这里的逻辑就是使用节点 key 创建了映射表,然后使用对比查找的方式,根据不同情况执行复用、删除、新增等操作。
具体分析:
1、什么是虚拟DOM?
虚拟 DOM 是真实 DOM 的 JS 对象,是对真实 DOM 的抽象,至少包含标签名(tagName)、标签的属性(props)以及子标签列表(children)
如下图,左侧是虚拟 DOM,对应的虚拟DOM就是右侧的JS对象。
基本上所有框架都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM
2、虚拟 DOM 的好处
① 减少对真实 DOM 的操作
页面的性能问题,大部分是由 DOM 操作引起的。真实的DOM 节点,哪怕一个最简单的 div 也包含着很多属性,操作 DOM 的代价仍旧是昂贵的。频繁操作还是会出现页面卡顿,影响用户的体验。
你用传统的原生 api 或 jQuery 去操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。而虚拟 DOM 不会立即操作 DOM ,底层经过 diff 算法得出一些需要修改的最小单位,再去更新视图,减少了 DOM 操作从而提高了性能。
将这次更新的 diff 内容保存到本地的一个
js
对象中,最终将这个js
对象一次性attach
到DOM 树上,避免大量的无谓计算。
在代码渲染到页面之前,Vue 会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实 DOM 结构,最终渲染到页面。在每次数据发生变化前,虚拟 DOM 都会缓存一份,变化之时,现在的虚拟 DOM 会与缓存的虚拟 DOM进行比较。在 Vue 内部封装了 diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改 DOM 的重绘重排次数,提高渲染性能。
看一下页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler
对比一下修改 DOM 时真实 DOM 操作和 Virtual DOM 的过程,以及它们重排重绘的性能消耗∶
- 真实 DOM∶ 生成 HTML 字符串+重建所有的 DOM 元素
- 虚拟 DOM∶ 生成 VNode + DOMDiff+必要的 DOM 更新
Virtual DOM 的更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。
尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
② 支持跨平台
抽象了原本的渲染过程,不依赖真实平台环境,从而实现了跨平台的能力。
从本质上来说,Virtual Dom是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。
它设计的最初目的,就是更好的跨平台,比如 Node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 DOM,因为虚拟 DOM 本身是 js 对象。
不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,也可以是小程序等等。
像以下2个框架,就是使用了虚拟 DOM 实现了跨平台的能力:
React-Native
和Weex
3、VDOM (虚拟DOM)是如何生成的/解析过程 ?
在 Vue 中我们常常会为组件编写模板 template,这个模板会被编译器编译为渲染函数,在挂载过程中会调用 render 函数,返回的对象就是虚拟 DOM,会在后续的 patch 过程中进一步转化为 真实DOM。
虚拟 DOM 由 Vue 组件的 render 函数生成,它描述了组件的结构和状态。
当组件的状态发生变化时,Vue 会重新渲染虚拟 DOM。
虚拟 DOM 的解析过程:
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来。比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
4、VDOM 如何做 diff 的?
① 组件挂载时会调用 render 函数生成虚拟 DOM,记录第一次生成的虚拟 DOM-oldVnode。
② 而当响应式数据发生变化时,会引起组件重新 触发 render 函数,生成新的虚拟 DOM- newVnode。
③ 老虚拟节点(oldVnode )与 新虚拟节点(newVnode )做 diff 算法操作,快速找到这两个 js 对象之前的差异,就可以最小化的高效更新视图。
5、什么是 diff 算法?
Vue 中 diff 算法原理_小草莓蹦蹦跳的博客-CSDN博客
首先,真实DOM会有对应的虚拟DOM,当真实DOM发生修改的时候,就会生成一个新的虚拟DOM,新旧两个虚拟DOM之间是存在一定差异的,如果能快速找到这两个js对象之前的差异,就可以最小化的更新视图。
而比对新旧虚拟DOM的差异就是 diff 算法,目的就是找出差异,使最小化的更新视图。
所以,diff 算法本质上就是比较两个JS对象的差异。
比对过程:
介绍一下updateChildren方法:
总结:
如果两个节点不相同的话,那么直接删除老节点,然后创建新节点。
如果是相同节点,那么会比较两个节点的差异,包括节点的 key 属性、tag 标签、事件等等;
如果两个父节点相同的话,那么就比较儿子节点。
比较子节点有以下4种情况:
- 如果都是文本节点且不相等,直接将
el
文本节点更新为新节点的文本内容即可;- 如果老的有儿子,新的没儿子,直接删除老的儿子节点;
- 如果老的没儿子,新的有儿子,那么就创造新的儿子节点,直接插入父节点中;
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点。
其中,diff 算法的核心是两个都有儿子的情况,这里采用的是双指针和优化比较的策略,优先头头、尾尾、头尾、尾头比较,优化了我们经常使用的 DOM 操作。那若是乱序的情况下,这里的逻辑就是使用节点 key 创建了映射表,然后使用对比查找的方式,根据不同情况执行复用、删除、新增等操作。
在这期间循环向中间靠拢,根据情况调用
patchVnode
进行patch
重复流程、调用createElem
创建一个新节点,从哈希表寻找key
一致的VNode
节点再分情况操作。