一篇文章带你搞懂Vue虚拟Dom与diff算法


Posted in Javascript onAugust 25, 2020

前言

使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色。由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它。

虚拟DOM

什么是虚拟DOM?

虚拟DOM(Virtual   Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的。
真实的元素节点:

<div id="wrap">
 <p class="title">Hello world!</p>
</div>

VNode:

{
 tag:'div',
 attrs:{
 id:'wrap'
 },
 children:[
 {
  tag:'p',
  text:'Hello world!',
  attrs:{
  class:'title',
  }
 }
 ]
}

为什么使用虚拟DOM?

简单了解虚拟DOM后,是不是有小伙伴会问:Vue和React框架中为什么会用到它呢?好问题!那来解决下小伙伴的疑问。
起初我们在使用JS/JQuery时,不可避免的会大量操作DOM,而DOM的变化又会引发回流或重绘,从而降低页面渲染性能。那么怎样来减少对DOM的操作呢?此时虚拟DOM应用而生,所以虚拟DOM出现的主要目的就是为了减少频繁操作DOM而引起回流重绘所引发的性能问题的!

虚拟DOM的作用是什么?

  1. 兼容性好。因为Vnode本质是JS对象,所以不管Node还是浏览器环境,都可以操作;
  2. 减少了对Dom的操作。页面中的数据和状态变化,都通过Vnode对比,只需要在比对完之后更新DOM,不需要频繁操作,提高了页面性能;

虚拟DOM和真实DOM的区别?

说到这里,那么虚拟DOM和真实DOM的区别是什么呢?总结大概如下:

  • 虚拟DOM不会进行回流和重绘;
  • 真实DOM在频繁操作时引发的回流重绘导致性能很低;
  • 虚拟DOM频繁修改,然后一次性对比差异并修改真实DOM,最后进行依次回流重绘,减少了真实DOM中多次回流重绘引起的性能损耗;
  • 虚拟DOM有效降低大面积的重绘与排版,因为是和真实DOM对比,更新差异部分,所以只渲染局部;

总损耗 = 真实DOM增删改 + (多节点)回流/重绘;    //计算使用真实DOM的损耗
总损耗 = 虚拟DOM增删改 + (diff对比)真实DOM差异化增删改 + (较少节点)回流/重绘;   //计算使用虚拟DOM的损耗

可以发现,都是围绕频繁操作真实DOM引起回流重绘,导致页面性能损耗来说的。不过框架也不一定非要使用虚拟DOM,关键在于看是否频繁操作会引起大面积的DOM操作。

那么虚拟DOM究竟通过什么方式来减少了页面中频繁操作DOM呢?这就不得不去了解DOM Diff算法了。

DIFF算法

当数据变化时,vue如何来更新视图的?其实很简单,一开始会根据真实DOM生成虚拟DOM,当虚拟DOM某个节点的数据改变后会生成一个新的Vnode,然后VNode和oldVnode对比,把不同的地方修改在真实DOM上,最后再使得oldVnode的值为Vnode。

diff过程就是调用patch函数,比较新老节点,一边比较一边给真实DOM打补丁(patch);

对照vue源码来解析一下,贴出核心代码,旨在简单明了讲述清楚,不然小编自己看着都头大了O(∩_∩)O

patch

那么patch是怎样打补丁的?

//patch函数 oldVnode:老节点 vnode:新节点
function patch (oldVnode, vnode) {
 ...
 if (sameVnode(oldVnode, vnode)) {
 patchVnode(oldVnode, vnode) //如果新老节点是同一节点,那么进一步通过patchVnode来比较子节点
 } else {
 /* -----否则新节点直接替换老节点----- */
 const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
 let parentEle = api.parentNode(oEl) // 父元素
 createEle(vnode) // 根据Vnode生成新元素
 if (parentEle !== null) {
  api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
  api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
  oldVnode = null
 }
 }
 ...
 return vnode
}

//判断两节点是否为同一节点
function sameVnode (a, b) {
 return (
 a.key === b.key && // key值
 a.tag === b.tag && // 标签名
 a.isComment === b.isComment && // 是否为注释节点
 // 是否都定义了data,data包含一些具体信息,例如onclick , style
 isDef(a.data) === isDef(b.data) && 
 sameInputType(a, b) // 当标签是<input>的时候,type必须相同
 )
}

从上面可以看出,patch函数是通过判断新老节点是否为同一节点:

  • 如果是同一节点,执行patchVnode进行子节点比较;
  • 如果不是同一节点,新节点直接替换老节点;

那如果不是同一节点,但是它们子节点一样怎么办嘞?OMG,要牢记:diff是同层比较,不存在跨级比较的!简单提一嘴,React中也是如此,它们只是针对同一层的节点进行比较。

patchVnode

既然到了patchVnode方法,说明新老节点为同一节点,那么这个方法做了什么处理?

function patchVnode (oldVnode, vnode) {
 const el = vnode.el = oldVnode.el  //找到对应的真实DOM
 let i, oldCh = oldVnode.children, ch = vnode.children 
 if (oldVnode === vnode) return  //如果新老节点相同,直接返回
 if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
 //如果新老节点都有文本节点且不相等,那么新节点的文本节点替换老节点的文本节点
 api.setTextContent(el, vnode.text) 
 }else {
 updateEle(el, vnode, oldVnode)
 if (oldCh && ch && oldCh !== ch) {
  //如果新老节点都有子节点,执行updateChildren比较子节点[很重要也很复杂,下面展开介绍]
  updateChildren(el, oldCh, ch)
 }else if (ch){
  //如果新节点有子节点而老节点没有子节点,那么将新节点的子节点添加到老节点上
  createEle(vnode)
 }else if (oldCh){
  //如果新节点没有子节点而老节点有子节点,那么删除老节点的子节点
  api.removeChildren(el)
 }
 }
}

如果两个节点不一样,直接用新节点替换老节点;

如果两个节点一样,

  • ​新老节点一样,直接返回;
  • ​老节点有子节点,新节点没有:删除老节点的子节点;
  • 老节点没有子节点,新节点有子节点:新节点的子节点直接append到老节点;
  • ​都只有文本节点:直接用新节点的文本节点替换老的文本节点;
  • ​都有子节点:updateChildren

最复杂的情况也就是新老节点都有子节点,那么updateChildren是如何来处理这一问题的,该方法也是diff算法的核心,下面我们来了解一下!

updateChildren

由于代码太多了,这里先做个概述。updateChildren方法的核心:

  1. 提取出新老节点的子节点:新节点子节点ch和老节点子节点oldCh;
  2. ch和oldCh分别设置StartIdx(指向头)和EndIdx(指向尾)变量,它们两两比较(按照sameNode方法),有四种方式来比较。如果4种方式都没有匹配成功,如果设置了key就通过key进行比较,在比较过程种startIdx++,endIdx--,一旦StartIdx > EndIdx表明ch或者oldCh至少有一个已经遍历完成,此时就会结束比较。

下面结合图来理解:

一篇文章带你搞懂Vue虚拟Dom与diff算法

第一步:

oldStartIdx = A , oldEndIdx = C;
newStartIdx = A , newEndIdx = D;

此时oldStartIdx和newStarIdx匹配,所以将dom中的A节点放到第一个位置,此时A已经在第一个位置,所以不做处理,此时真实DOM顺序:A  B  C;

第二步:

oldStartIdx = B , oldEndIdx = C;
newStartIdx = C , oldEndIdx = D;

一篇文章带你搞懂Vue虚拟Dom与diff算法

此时oldEndIdx和newStartIdx匹配,将原本的C节点移动到A后面,此时真实DOM顺序:A   C   B;

第三步:

oldStartIdx = C , oldEndIdx = C;
newStartIdx = B , newEndIdx = D;
oldStartIdx++,oldEndIdx--;
oldStartIdx > oldEndIdx

此时遍历结束,oldCh已经遍历完,那么将剩余的ch节点根据自己的index插入到真实DOM中即可,此时真实DOM顺序:A  C  B  D;

所以匹配过程中判断结束有两个条件:

  • oldStartIdx > oldEndIdx表示oldCh先遍历完成,如果ch有剩余节点就根据对应index添加到真实DOM中;
  • newStartIdx > newEndIdx表示ch先遍历完成,那么就要在真实DOM中将多余节点删除掉;

看下图这个实例,就是新节点先遍历完成删除多余节点:

一篇文章带你搞懂Vue虚拟Dom与diff算法

最后,在这些子节点sameVnode后如果满足条件继续执行patchVnode,层层递归,直到oldVnode和Vnode中所有子节点都比对完成,也就把所有的补丁都打好了,此时更新到视图。

总结

最后,用一张图来记忆整个Diff过程,希望你能有所收获!

一篇文章带你搞懂Vue虚拟Dom与diff算法

彩蛋

因为React只是简单学了基础,这里作为对比来概述一下:

1.React渲染机制:React采用虚拟DOM,在每次属性和状态发生变化时,render函数会返回不同的元素树,然后对比返回的元素树和上次渲染树的差异并对差异部分进行更新,最后渲染为真实DOM。

2.diff永远都是同层比较,如果节点类型不同,直接用新的替换旧的。如果节点类型相同,就比较他们的子节点,依次类推。通常元素上绑定的key值就是用来比较节点的,所以一定要保证其唯一性,一般不采用数组下标来作为key值,因为当数组元素发生变化时index会有所改动。

3.渲染机制的整个过程包含了更新操作,将虚拟DOM转换为真实DOM,所以整个渲染过程就是Reconciliation。而这个过程的核心又主要是diff算法,利用的是生命周期shouldComponentUpdate函数。

到此这篇带你搞懂Vue虚拟Dom与diff算法的文章就介绍到这了,更多相关Vue虚拟Dom与diff算法内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript 简练的几个函数
Aug 29 Javascript
JavaScript 产生不重复的随机数三种实现思路
Dec 13 Javascript
鼠标移到导航当前位置的LI变色处于选中状态
Aug 23 Javascript
jquery解析JSON数据示例代码
Mar 17 Javascript
使用jquery操作session方法分享
Jan 22 Javascript
js实现多选项切换导航菜单的方法
Feb 06 Javascript
JavaScript常用脚本汇总(二)
Mar 04 Javascript
JavaScript实现鼠标滑过图片变换效果的方法
Apr 16 Javascript
Extjs实现下拉菜单效果
Apr 01 Javascript
JavaScript基础教程——入门必看篇
May 20 Javascript
Angular 4.0学习教程之架构详解
Sep 12 Javascript
AngularJS实现图片上传和预览功能的方法分析
Nov 08 Javascript
微信小程序换肤功能实现代码(思路详解)
Aug 25 #Javascript
prettier自动格式化去换行的实现代码
Aug 25 #Javascript
Vue中 axios delete请求参数操作
Aug 25 #Javascript
React实现轮播效果
Aug 25 #Javascript
React实现全选功能
Aug 25 #Javascript
react实现复选框全选和反选组件效果
Aug 25 #Javascript
js实现数字跳动到指定数字
Aug 25 #Javascript
You might like
基于ubuntu下nginx+php+mysql安装配置的具体操作步骤
2013/04/28 PHP
PHP的拦截器实例分析
2014/11/03 PHP
Yii2创建表单(ActiveForm)方法详解
2016/07/23 PHP
php PDO属性设置与操作方法分析
2018/12/27 PHP
PHP array_shift()用法实例分析
2019/01/07 PHP
php过滤htmlspecialchars() 函数实现把预定义的字符转换为 HTML 实体用法分析
2019/06/25 PHP
javascript 模拟JQuery的Ready方法实现并出现的问题
2009/12/06 Javascript
JavaScript取得鼠标绝对位置程序代码介绍
2012/09/16 Javascript
好好了解一下Cookie(强烈推荐)
2016/06/14 Javascript
jquery表单提交带错误信息提示效果
2017/03/09 Javascript
JS动态添加的div点击跳转到另一页面实现代码
2017/09/30 Javascript
ES6 javascript中class静态方法、属性与实例属性用法示例
2017/10/30 Javascript
详解Vue路由钩子及应用场景(小结)
2017/11/07 Javascript
Angular5升级RxJS到5.5.3报错:EmptyError: no elements in sequence的解决方法
2018/04/09 Javascript
详解jQuery获取特殊属性的值以及设置内容
2018/11/14 jQuery
Vue Prop属性功能与用法实例详解
2019/02/23 Javascript
vue-cli 3.x配置跨域代理的实现方法
2019/04/12 Javascript
微信小程序实现动态列表项的顺序加载动画
2019/07/25 Javascript
基于javascript实现日历功能原理及代码实例
2020/05/07 Javascript
JavaScript oncopy事件用法实例解析
2020/05/13 Javascript
[01:25]2014DOTA2国际邀请赛 zhou分析LGD比赛情况
2014/07/14 DOTA
[48:52]DOTA2上海特级锦标赛A组小组赛#2 Secret VS CDEC第一局
2016/02/25 DOTA
调用其他python脚本文件里面的类和方法过程解析
2019/11/15 Python
Python:type、object、class与内置类型实例
2019/12/25 Python
pandas分批读取大数据集教程
2020/06/06 Python
python开发入门——列表生成式
2020/09/03 Python
matplotlib 画动态图以及plt.ion()和plt.ioff()的使用详解
2021/01/05 Python
Space NK美国站:英国高端美妆护肤商城
2017/05/22 全球购物
西班牙电子产品购物网站:Electronicamente
2018/07/26 全球购物
shell程序中如何注释
2012/02/17 面试题
电子商务求职信
2014/06/15 职场文书
学校党的群众路线教育实践活动领导班子对照检查材料
2014/09/25 职场文书
laravel ajax curd 搜索登录判断功能的实现
2021/04/17 PHP
手把手教你怎么用Python实现zip文件密码的破解
2021/05/27 Python
Python Django ORM连表正反操作技巧
2021/06/13 Python
Vue提供的三种调试方式你知道吗
2022/01/18 Vue.js