一篇文章带你搞懂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 indexOf函数使用说明
Jul 03 Javascript
Firefox+FireBug使JQuery的学习更加轻松愉快
Jan 01 Javascript
JavaScript  cookie 跨域访问之广告推广
Apr 20 Javascript
javascript轮播图算法
Oct 21 Javascript
遍历json 对象的属性并且动态添加属性的实现
Dec 02 Javascript
prototype与__proto__区别详细介绍
Jan 09 Javascript
详解使用Node.js 将txt文件转为Excel文件
Jul 05 Javascript
jQuery的时间datetime控件在AngularJs中的使用实例(分享)
Aug 17 jQuery
vue实现侧边栏导航效果
Oct 21 Javascript
JavaScript中this的学习笔记及用法整理
Feb 17 Javascript
在vue中实现禁止屏幕滚动,禁止屏幕滑动
Jul 22 Javascript
Vue中避免滥用this去读取data中数据
Mar 02 Vue.js
微信小程序换肤功能实现代码(思路详解)
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 高手之路(二)
2006/10/09 PHP
搜索和替换文件或目录的一个好类--很实用
2006/10/09 PHP
ThinkPHP php 框架学习笔记
2009/10/30 PHP
JavaScript实现删除电脑的关机键
2016/07/26 PHP
redis+php实现微博(三)微博列表功能详解
2019/09/23 PHP
JavaScript 高级语法介绍
2009/06/15 Javascript
js当一个变量为函数时 应该注意的一点细节小结
2011/12/29 Javascript
jquery 倒计时效果实现秒杀思路
2013/09/11 Javascript
jQuery多项选项卡的实现思路附样式及代码
2014/06/03 Javascript
Ionic如何实现下拉刷新与上拉加载功能
2016/06/03 Javascript
socket.io学习教程之基本应用(二)
2017/04/29 Javascript
javascript将json格式数组下载为excel表格的方法
2017/12/22 Javascript
Vue.directive()的用法和实例详解
2018/03/04 Javascript
Angular4学习笔记router的简单使用
2018/03/30 Javascript
在vue-cli搭建的项目中增加后台mock接口的方法
2018/04/26 Javascript
对angularJs中controller控制器scope父子集作用域的实例讲解
2018/10/08 Javascript
详解如何实现Element树形控件Tree在懒加载模式下的动态更新
2019/04/25 Javascript
js实现弹幕飞机效果
2020/08/27 Javascript
[01:03:27]Optic vs VGJ.S 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
Python实现简单状态框架的方法
2015/03/19 Python
Python自动化部署工具Fabric的简单上手指南
2016/04/19 Python
深入了解Python中pop和remove的使用方法
2018/01/09 Python
wxpython实现图书管理系统
2018/03/12 Python
使用Python操作FTP实现上传和下载的方法
2019/04/01 Python
Pytest mark使用实例及原理解析
2020/02/22 Python
Django实现celery定时任务过程解析
2020/04/21 Python
复古斯堪的纳维亚儿童服装:Baby go Retro
2017/09/09 全球购物
安德玛加拿大官网:Under Armour加拿大
2019/10/02 全球购物
德国家具折扣店:POCO
2020/02/28 全球购物
自我评价个人范文
2013/12/16 职场文书
校园新闻广播稿
2014/01/10 职场文书
班级学习计划书
2014/04/27 职场文书
村抢险救灾方案
2014/05/09 职场文书
公司爱心捐款倡议书
2014/05/14 职场文书
第一节英语课开场白
2015/06/01 职场文书
MySQL中VARCHAR与CHAR格式数据的区别
2021/05/26 MySQL