聊一聊虚拟dom和diff算法
今天没事了,聊聊虚拟 Dom 和 Diff 算法吧。
首先我们在讲这个之前,来说一说,作者对于 Vue 和 React 的一个理解。方便读者自己再吸收再理解。
我们每次创建 react 项目或者 vue 项目的时候,入口文件 App.js / main.js 都会去创建一个 React 对象/ Vue 对象,然后我们能够看到这样一段代码
或者这样的
其实看到了这里我们不妨深入想一点,这其实本质上,是不是我们去画一个页面的交互逻辑,核心其实就是在这个 React 对象/ Vue 对象上去实现我们所有需要的逻辑对象呢?又像不像我们常说的面向对象的编程思想。所有的逻辑都基于这个 React 对象/ Vue 对象。
好了!!!延伸打住,请同学们自行思考,我们开始讲正题了
这篇文章主要基于 Vue 的源码来进行讲解,最后会放上 React 的部分源码。大家有兴趣的可以去看看。
首先来说,为啥实现虚拟 Dom 众所周知,创建一个真实Dom快还是一个Js对象节点快?
答案是:后者。
这样我们直指问题的核心,也是很多面试官会问的,为啥要用虚拟 Dom 和Diff算法?
答案:因为它快!!!
而后,第二个问题又来了 ...
虚拟 Dom 与 Diff 算法结合一定比操作真实的 Dom 快吗?
答案:不一定哦!!!
为什么呢?我们往下讲,大家就会明白了
虚拟Dom的本质其实就是利用 Js 对象的来模拟创建一个 Dom 节点,
这是真实的 Dom
虚拟Dom
其实,本质上我们就是把html标签通过编译器转义成了一个 js 对象,然后去通过 diff 算法去进行比对Dom的变化,实现从全局渲染到局部渲染的一个过程。至于为了什么!!!
哦,还是那个答案:因为它快、天下武功,唯坚不摧,唯快不破;
然后回到刚才那个问题,虚拟一定比真实快嘛?
答案:快是有条件的,你就俩 Dom 元素,直接操作永远比虚拟快,可是,我们的项目是什么呢?大部分都是 屎山plus ,所以,虚拟很快。
分割线-------------------------------------------------------------------------
PS : 不要说将来,至少在目前,数据驱动的框架 Vue / React 是主流,Svelte 很强,可是我选择 Vue / React
#######################################################
回归正题:
通过上面的代码我们可以看出来所谓的虚拟 Dom 本质上就是用一个js对象的形式来模拟出一个Dom节点;
那么我们往下讲,虚拟Dom的好兄弟,Diff算法了,
以Vue为例,完成虚拟Dom的实现一共有以下几个函数:
########################################################
Vnode
----------------------------------------------------------------------------
这是第一个函数,首先看名字,我们已经能够了解到,这是Virtual Dom的一个简写就是我们所说的虚拟dom的生成,简单点不用看那么多参数记住几个关键点,tag/标签,data/节点参数,children/子节点,text/文本节点,elm/对应的真实 dom 节点,key/生成的绑定的 key 参数值。
记住这些参数这就是我们最基本的一个 Virtual Dom 的一个节点状态。而后,剩下的所有函数的根本都是为了他们服务的。也是 Diff 算法所针对的根本核心对象
----------------------------------------------------------------------------
########################################################
patch
----------------------------------------------------------------------------
第二个关键函数patch,我们可能看到这么多的参数和函数调用会产生头大的感觉,么得慌,我们还是用最简单的办法,透过现象看本质,对于初学者来说,没有必要一定说弄懂每个参数函数的概念,只需要抓住几个关键点。来理解意图,这样就能在面试中回答到面试官的问题。
切入正题开始有两个参数,oldVnode / Vnode,首先我们一看这两个参数是不是就能明白这两个参数代表什么是不是代表说我们的 旧节点 与 新节点 ,然后向下看,isUndef / isDef 这两个函数是用来判断节点是否为空,由于作者比较懒,就不贴源码了,有兴趣的自己看。如果为空那就直接返回,不用判断了。
OK,剩下我们继续看,看到 patchVnode 之前,这段参数简单概括就可以说,不停的进行判断,判断节点是否为空,是否存在,类型是否改变,是否有对应真实节点。
而patchVonde之后的代码,则是继续判断,节点父级的参数,不过这里其实对于我们的核心没有太大影响。这个地方其实本质就是个入口函数
看到这里,其实说代码会很复杂,但是一归纳其实就是不停判断节点的可渲染性,缩小范围。根据我们节点的参数不同,来进行相应的操作。作者个人理解,可以说是相当于一个入口函数,然后根据参数不同,进行分流操作。
----------------------------------------------------------------------------
########################################################
sameVnode
----------------------------------------------------------------------------
然后sameVnode这个函数,其实这个函数也很简单,我们来看代码,做了几步操作,这个地方传入的 a / b 参数其实就是 oldVnode / Vnode 这两个节点,然后我们看比对了什么, key / tag / isComment / asyncFactory 四大项参数,key / tag刚才说过了标签和循环 key 然后isComment代表是否为注释组件,asyncFactory是节点绑定回调事件,
看完这几个参数,我们简单总结一下,本质就是我们的一个节点一个比对。可以说是还是我们在进行Diff比对之前的一个执行判断。那么这个先讲到这里,我们继续向下看。
----------------------------------------------------------------------------
########################################################
patchVnode
----------------------------------------------------------------------------
pacthVnode 函数,首先我们打眼溜一遍,是不是全部都是 if 判断,那么在看清一色的 Vnode / oldVnode 开头,那么是不是可以浅猜一下,是不是对于我们的虚拟dom的一个根本性的比对?
然后我们细看一看,我们比对了什么?isStatic / data / children / text / elm等参数,是不是我们之前看到的 Vnode 构造函数中创建的 js 对象?那么透过现象看本质,是不是说我们这个patchVnode函数的功能就是对于我们的Virltual Dom对象做了一个深层次的比对?
OK?那么我们继续向下,该看最根本的核心了。也是 Diff 的核心代码了
----------------------------------------------------------------------------
########################################################
updateChildren
----------------------------------------------------------------------------
来吧,最核心的代码来了,Diff的根本逻辑,也是vue的强化之处,所谓双指针遍历,这个地方我们稍稍讲细点,首先看传进来的参数oldch/newch,
然后创建了 startIdx / endIdx这四个参数,这是四个指针我们可以先这么理解,oldch 两个一个头一个尾,newch 两个一个头一个尾,然后我们看,下面用 while 进行了循环比对,每次进行比对四种情况,首首/尾尾/首尾/首尾。然后比对完成之后在进行 sameVnode / patchVnode函数再调用,循环递归遍历参数直至下方不再含有子节点。
最后,我们们将比对完成的节点进行渲染,将不存在或已经消失的节点进行删除,完成我们的节点重绘。
----------------------------------------------------------------------------
########################################################
以上呢是我们的vue的一个Virtual Dom和Diff算法的一个源码的贴图,和最基础逻辑的一个讲解。本来想着逐条代码解析,后来发现太多了,作者又比较懒,就归纳性的给大家讲一下一些逻辑概念吧。
然后总结一下吧。Virtual Dom 和 Diff的步骤吧。
1、创建虚拟Dom;
2、当我们的Dom节点发生改变的时候。进行比对
3、调用patch进行分流比对。是否发生变化?
4、调用patchVnode进行深层比对,子节点是否发生变化?
5、如果子节点发生变化、调用updateChildren进行循环遍历,在循环遍历过程中判断是否含有子节点?如果有,递归调用patchVnode继续循环比对。
6、比对完成之后,将改变的新 Dom。add进去真实节点中,同时移除无用的节点。完成Diff。
最后,漏了一个点,就是关于key,为什么要用key不能用index?和key的作用?
简单讲一下吧
1、为什么要用key不能用index?
因为key是每次指针遍历循环时判断是否存在节点的依据。key需要的是一个常量参数不得改变,而index在参数数组改变时会发生改变,导致相同节点key值不同会进行重复比对,性能优化基本全部浪费掉了。
2、key的作用?
key的作用其实上面也说了,就是判断节点是否有相同的依据,如果有,直接判断节点内部参数就OK,不需要比对节点本身了。在相同数组,部分数据改变的情况下,利用key可以实现针对部分数据的重新比对,不必再次比对所有节点,完成性能优化。如果key不存在的话,那么每次Diff比对时,则会将所有节点重新比对一次。性能优化同样为0。
然后写到现在比较累了,react的源码不贴了,逻辑核心是一样的,有几点不同,然后给大家贴个总结,了解一下
Vue / React 的 Diff 相同点与不同点:
共同点:vue和diff算法,都是不进行跨层级比较,只做同级比较
不同点:
1.vue进行diff时,调用patch打补丁函数,一边比较一边给真实的dom打补丁,vue对比节点时,当节点元素类型相同,类名不同时,认为是不同的元素,删除重新创建,而react认为是同类型的节点,进行修改操作
2.vue列表对比的时候,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,每次对比结束后,指针向队列中间移动;react则是从左往右一次对比,利用元素的index和lastindex进行比较
3.当一个集合把最后一个节点移动到最前面,react会把前面的节点依次向后移动,而Vue只会把最后一个节点放在最前面,这样的操作来看,Vue的diff性能是高于react的。
886!!!