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

前言

使用过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至少有一个已经遍历完成,此时就会结束比较。

下面结合图来理解:

第一步:

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;

此时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中将多余节点删除掉;

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

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

总结

最后,用一张图来记忆整个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算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue的transition-group与Virtual Dom Diff算法的使用

    开始 这次的题目看上去好像有点奇怪:把两个没有什么关联的名词放在了一起,正如大家所知道的,transition-group就是Vue的内置组件之一主要用在列表的动画上,但是会跟Virtual Dom Diff算法有什么特别的联系吗?答案明显是有的,所以接下来就是代码分解. 缘起 主要是最近对Vue的Virtual Dom Diff算法有点模糊了,然后顺手就打开了电脑准备温故知新:但是很快就留意到代码: // removeOnly is a special flag used only by <t

  • 深入理解Vue2.x的虚拟DOM diff原理

    前言 经常看到讲解Vue2的虚拟Dom diff原理的,但很多都是在原代码的基础上添加些注释等等,这里从0行代码开始实现一个Vue2的虚拟DOM 实现VNode src/core/vdom/Vnode.js export class VNode{ constructor ( tag, //标签名 children,//孩子[VNode,VNode], text, //文本节点 elm //对应的真实dom对象 ){ this.tag = tag; this.children = children

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

    前言 使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色.由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它. 虚拟DOM 什么是虚拟DOM? 虚拟DOM(Virtual   Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的. 真实的元素节点: <div id="wr

  • 一篇文章带你搞懂VUE基础知识

    目录 VUE是什么 Vue中的核心插件 Vue Router Vuex axios element-ui Vue前端整体架构 总结 VUE是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用.Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合.另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动. Vue使用了双向数据绑定,即

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你搞懂Java线程池实现原理

    目录 1. 为什么要使用线程池 2. 线程池的使用 3. 线程池核心参数 4. 线程池工作原理 5. 线程池源码剖析 5.1 线程池的属性 5.2 线程池状态 5.3 execute源码 5.4 worker源码 5.5 runWorker源码 1. 为什么要使用线程池 使用线程池通常由以下两个原因: 频繁创建销毁线程需要消耗系统资源,使用线程池可以复用线程. 使用线程池可以更容易管理线程,线程池可以动态管理线程个数.具有阻塞队列.定时周期执行任务.环境隔离等. 2. 线程池的使用 /** *

  • 一篇文章带你搞懂JavaScript的变量与数据类型

    目录 前言: 温馨提示: 变量 1.声明 2.赋值 3.二个语法小细节 变量的命名规范 为什么需要数据类型? 简单数据类型(基本数据类型) 数字型 字符串型 String 什么是数据类型的转换 1.转换为字符串 2.转换为数字型(重点) 转化为布尔型 总结 前言: 我不是搞前端,而是搞后端的.本命编程语言是java.学习js的嘛,因为看到室友能做出动态网页,而我只能做出静态网页,再加上下个学期要学所以提前来学习学习. 温馨提示: java和javsScript没有半毛钱关系,只是javaScri

  • 一篇文章带你搞懂Go语言标准库Time

    目录 前言 时间类型 时间戳 时间间隔 操作时间 Add Sub Equal Before After 定时器 时间格式化 解析字符串格式的时间 总结 前言 在编程过程中,我们经常会用到与时间和日期相关的各种需求,下面来介绍 Go 语言中有关时间的一些基本用法. 时间类型 time.Time 类型表示时间.可以通过 time.Now() 函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息. 示例代码如下: func main() { now := time.Now() //获取当前时间

  • 一篇文章带你搞懂Java restful 接口开发

    目录 1.RESTful 简介 a>资源 b>资源的表述 c>状态转移 2.RESTful 的实现 3.HiddenHttpMethodFilter 4.RESTful 案例 4.1.需求 4.2.git 代码位置 4.3.UserController 4.4.添加 HiddenHttpMethodFilter 4.5.测试效果 总结 1.RESTful 简介 REST:Representational State Transfer,表现层资源状态转移. a>资源 资源是一种看待服

  • vue中虚拟DOM与Diff算法知识精讲

    目录 前言 知识点: 虚拟DOM(Virtual DOM): 虚拟dom库 diff算法 snabbdom的核心 init函数 h函数 patch函数(核心) 题外话:diff算法简介 传统diff算法 snabbdom的diff算法优化 updateChildren(核中核:判断子节点的差异) 新结束节点和旧结束节点(情况2) 旧结束节点/新开始节点(情况4) 前言 面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们"; 我:"额,...鹅

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定SpringBoot不重启项目实现修改静态资源

    一.通过配置文件控制静态资源的热部署 在配置文件 application.properties 中添加: #表示从这个默认不触发重启的目录中除去static目录 spring.devtools.restart.exclude=classpath:/static/** 或者使用: #表示将static目录加入到修改资源会重启的目录中来 spring.devtools.restart.additional-paths=src/main/resource/static 此时对static 目录下的静态

随机推荐