一篇文章带你搞懂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 相关文章推荐
基于jquery的回到页面顶部按钮
Jun 27 Javascript
jquery.fileEveryWhere.js 一个跨浏览器的file显示插件
Oct 24 Javascript
JQuery入门—JQuery程序的代码风格详细介绍
Jan 03 Javascript
javascript 模拟坦克大战游戏(html5版)附源码下载
Apr 08 Javascript
Egret引擎开发指南之创建项目
Sep 03 Javascript
JQuery插件Quicksand实现超炫的动画洗牌效果
May 03 Javascript
如何用js 实现依赖注入的思想,后端框架思想搬到前端来
Aug 03 Javascript
再谈Javascript中的异步以及如何异步
Aug 19 Javascript
JS制作图形验证码实现代码
Oct 19 Javascript
原生JS实现微信通讯录
Jun 18 Javascript
全面解析JavaScript Module模式
Jul 24 Javascript
解决vue项目axios每次请求session不一致的问题
Oct 24 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
基于PHP实现的事件机制实例分析
2015/06/18 PHP
PHP页面转UTF-8中文编码乱码的解决办法
2015/10/20 PHP
PHP使用Session实现上传进度功能详解
2019/08/06 PHP
jQuery渐变发光导航菜单的实例代码
2013/03/27 Javascript
Select标签下拉列表二级联动级联实例代码
2014/02/07 Javascript
Javascript验证上传图片大小[前台处理]
2014/07/18 Javascript
ie8下修改input的type属性报错的解决方法
2014/09/16 Javascript
用js判断是否为360浏览器的实现代码
2015/01/15 Javascript
JavaScript学习笔记之JS函数
2015/01/22 Javascript
利用CSS3在Angular中实现动画
2016/01/15 Javascript
用AngularJS来实现监察表单按钮的禁用效果
2016/11/02 Javascript
详解nodejs中的process进程
2017/03/19 NodeJs
基于React Native 0.52实现轮播图效果
2020/08/25 Javascript
JS使用队列对数组排列,基数排序算法示例
2019/03/02 Javascript
Vue.js递归组件实现组织架构树和选人功能
2019/07/04 Javascript
vue中使用vue-pdf的方法详解
2020/09/05 Javascript
初学Python实用技巧两则
2014/08/29 Python
Python中集合的内建函数和内建方法学习教程
2015/08/19 Python
Python字符串特性及常用字符串方法的简单笔记
2016/01/04 Python
Windows和Linux下Python输出彩色文字的方法教程
2017/05/02 Python
详解Django之admin组件的使用和源码剖析
2018/05/04 Python
在Python中实现替换字符串中的子串的示例
2018/10/31 Python
Python 实现一行输入多个数字(用空格隔开)
2020/04/29 Python
python实现每天自动签到领积分的示例代码
2020/08/18 Python
python3中celery异步框架简单使用+守护进程方式启动
2021/01/20 Python
Annoushka英国官网:英国奢侈珠宝品牌
2018/10/20 全球购物
写出程序把一个链表中的接点顺序倒排
2014/04/28 面试题
工商管理专业应届生求职信
2013/11/04 职场文书
数学专业毕业生自荐信
2013/11/10 职场文书
客房主管岗位职责
2013/12/09 职场文书
试用期转正鉴定评语
2014/01/27 职场文书
2014年学习厉行节约反对浪费思想汇报
2014/09/10 职场文书
同学会邀请函模板
2015/01/30 职场文书
工作失职检讨书范文
2015/05/05 职场文书
生死牛玉儒观后感
2015/06/11 职场文书
python 实现德洛内三角剖分的操作
2021/04/22 Python