vue的解析源码论坛越狱diff算法 VUE源码解析 面试者角度回答
在面试中,面试官可能会问起Vue中的源码diff算法。这个算法在组件依赖数据更新或初次创建时启动,解析主要在update函数中运行。源码首先,解析组件的源码render函数生成新的虚拟DOM树,然后更新函数将旧的解析_vnode替换为新树的根节点。接下来,源码diff算法通过一个名为patch的解析函数,遵循原则:尽可能保持不变,源码仅修改属性、移动DOM,最后实在不行才删除或新增真实DOM。
diff过程采用深度优先和同层比较策略。它首先比较标签名,接着是key值(对于input元素还会检查type),发现不同时,记录指针位置,逐渐聚拢,直到新虚拟DOM树的头尾指针相等,表示比对完成。在这个过程中,相同节点仅更新属性,不同节点则进行删除、新建或替换操作。key值的存在有助于提高真实DOM的复用效率。
diff的时间复杂度通过优化降低了从O(n3)到O(n),因为前端DOM操作通常限于同一层级,只对同级节点进行比较。Vue的diff算法核心是高效地在虚拟DOM和真实DOM之间进行更新。
diff在Vue中的应用是基于虚拟DOM的渲染更新。比如,tensorflow源码查看语句新旧VNode节点会逐层进行比较,通过添加、删除或移动真实DOM元素,确保视图与数据的一致性。当数据变化时,Dep.notify和patch函数协同工作,确保DOM的同步更新。
DIff算法看不懂就一起来砍我(带图)
面试官:“你对虚拟DOM(Virtual DOM)和Diff算法了解吗?请描述一下。”
我:“额,那个,嗯...”突然智商不在线,没组织好语言,没答好或者压根就答不出来。
所以这次我总结一下相关的知识点,让你可以有一个清晰的认知,也会让你在今后遇到这种情况可以坦然自若,应付自如,游刃有余:
相关知识点:
虚拟DOM(Virtual DOM):
Diff算法:
虚拟DOM(Virtual DOM):
什么是虚拟DOM:
一句话总结虚拟DOM就是一个用来描述真实DOM的JavaScript对象,这样说可能不够形象,那我们来举个:分别用代码来描述真实DOM以及虚拟DOM。
真实DOM:
对应的虚拟DOM:
控制台打印出来的Vnode:
h函数生成的虚拟DOM这个JS对象(Vnode)的源码:
补充:
上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆;开发中常见的现实场景,render函数渲染:
为什么要使用虚拟DOM:
灵魂发问:使用了虚拟DOM就一定会比直接渲染真实DOM快吗?答案当然是否定的,且听我说:
举例:当一个节点变更时DOMA->DOMB
上述情况:
示例1是创建一个DOMB然后替换掉DOMA;
示例2去创建虚拟DOM+Diff算法比对发现DOMB跟DOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA;
可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+Diff算法对比
所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的
举例:当DOM树里面的某个子节点的内容变更时:
当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例1重新去渲染这个DOM树,这个时候虚拟DOM+Diff算法就能够得到很好的体现,我们通过示例2使用虚拟DOM+Diff算法找出改变了的子节点更新它的内容就可以了
总结:复杂视图情况下提升渲染性能,因为虚拟DOM+Diff算法可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)
虚拟dom库Diff算法:
在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,ICN2053源码Diff算法其实就是找出两者之间的差异;
diff算法首先要明确一个概念就是Diff的对象是虚拟DOM(virtual dom),更新真实DOM是Diff算法的结果。
下面我将会手撕snabbdom源码核心部分为大家打开Diff的心,给点耐心,别关网页,我知道你们都是这样:
snabbdom的核心
init函数
init函数时设置模块,然后创建patch()函数,我们先通过场景案例来有一个直观的体现:
当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建虚拟DOM(Vnode)对象;在上文中就使用了样式模块以及事件模块让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过patch函数对比两个虚拟dom(会先把app转换成虚拟dom),更新视图;
我们再简单看看init的源码部分:
这些地方也会用createElement来命名,它们是一样的东西,都是创建虚拟DOM的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:
总结:h函数先生成一个vnode函数,然后vnode函数再生成一个Vnode对象(虚拟DOM对象)
补充:
在h函数源码部分涉及一个函数重载的概念,简单说明一下:
重载这个概念和参数相关,和返回值无关
patch函数(核心):
要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;
源码:
看得可能有点蒙蔽,下面再上一副思维导图:
题外话:Diff算法简介:
传统Diff算法
snabbdom的Diff算法优化
下面我们就会介绍updateChildren函数怎么去对比子节点的异同,也是Diff算法里面的一个核心以及难点;
updateChildren(核中核:判断子节点的差异):
为了更加直观的了解,我们再来看看同级别节点比较的五种情况的实现细节:
新开始节点和旧开始节点(情况1)新结束节点和旧结束节点(情况2)旧开始节点/新结束节点(情况3)旧结束节点/新开始节点(情况4)新开始节点/旧节点数组中寻找节点(情况5)
下面我们再介绍一下结束循环的收尾工作(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):
最后附上源码:
key的作用:
以下我们看看这些作用的实例:
Diff操作可以更加准确;(避免渲染错误)
实例:a、b、c三个DOM元素中的b、c间插入一个z元素
没有设置key
没有设置key
当设置了key:
Diff操作可以更加准确;(避免渲染错误)
实例:a、b、c三个DOM元素,修改了a元素的某个属性再去在a元素前新增一个z元素
没有设置key:
因为没有设置key,默认都是undefined,所以节点都是相同的,更新了text的内容但还是沿用了之前的DOM,所以实际上a->z(a原本打勾的状态保留了,只改变了text),抵押挖矿源码b->a,c->b,d->c,遍历完毕发现还要增加一个DOM,在最后新增一个text为d的DOM元素
设置了key:
当设置了key,a、b、c、d都有对应的key,a->a,b->b,c->c,d->d,内容相同无需更新,遍历结束,新增一个text为z的DOM元素
不推荐使用索引作为key:
设置索引为key:
这明显效率不高,我们只希望找出不同的节点更新,而使用索引作为key会增加运算时间,我们可以把key设置为与节点text为一致就可以解决这个问题:
最后:
如有描述错误或者不明的地方请在下方评论联系我,我会立刻更新,如有收获,请为我点个赞,这是对我的莫大的支持,谢谢各位。
深入浅出 虚拟DOM、Diff算法核心原理(源码解析)
五一假期后,笔者试图通过面试找到新工作,却意外地在Diff算法的挑战中受挫。为了不再在面试中尴尬,我熬夜研究了源码,希望能为即将面临同样挑战的朋友们提供一些帮助。
首先,让我们来理解什么是虚拟DOM。真实DOM的渲染过程是怎样的?为什么需要虚拟DOM?想象一下,每次DOM节点更新,浏览器都要重新渲染整个树,仿appstore下载源码这效率低下。虚拟DOM应运而生,它是一个JavaScript对象,用以描述DOM结构,包括标签、属性和子节点关系。
虚拟DOM的优点在于,通过Diff算法,它能对比新旧虚拟DOM,仅更新变动的部分,而非整个DOM,从而提升性能。Diff算法主要流程包括:对比旧新虚拟DOM的差异,确定需要更新的节点,然后仅更新这部分的真实节点。
例如在React、Vue等框架中,Vue2.x采用深度优先策略,而Vue3.x可能使用不同方法。核心的patch.js文件中,patchNode函数会处理添加、删除和更新子节点的情况,采用双端比较策略,确保高效更新。
虽然文章已在此打住,但思考题仍在:当新节点(newCh)比旧节点(oldCh)多时,如何处理多出的节点?试着模拟这个过程,通过画图理解,这将有助于深入理解Diff算法的工作原理。
React原理剖析之diff算法,一点也不难!!!
diff算法听起来很深奥。其实真的没那么难。今天我们就用最简单的话把diff算法讲清楚。虚拟Dom什么是虚拟Dom在传统的dom中只有真实Dom↓
这玩意就是真实DOM,我们能看见的节点元素。↓
react/vue这些组件化的框架诞生后,增加了一个概念,虚拟Dom。所以,在传统的浏览器和真实Dom之间增加了一层。虚拟Dom层。成了这样↓
虚拟DOM本质上他就只是一个对象!!!没什么难的,虚拟DOM就只是一个对象而已。只是这玩意,我们不能在视图上只管的看见,它在源码里。
我们拿一段真实DOM来映射一下与之对应的虚拟Dom↓
//真实DOM<divclass="d-class"key="d"><pclass="p-class">我是p元素</p></div>//与之对应的虚拟DOMconstvNode={ key:"d",//是否有key,有则显示,无则显示nullprops:{ //标签里是否子元素children:[{ type:'p',....},],onClick:()=>{ },//标签上的事件className:"d-class",//标签上的属性}ref:"null",type:"div"}vNode这种对象,这玩意就是react中虚拟DOM的真实面目。
为什么要有虚拟DOM因为重新从头生成个正正经经的DOM节点开销实在是太大了!!!从浏览器开始创建一个节点到这个节点完全渲染到视图上去,是比较耗费性能的,当一群节点都需要去创建并渲染的时候,极端情况下,会出现肉眼可见的卡顿。虚拟Dom的作用就是尽量减少元素的创建。
我们再来一个简单的流程解释一下
//dom结构一<divclass="d-class"><pclass="p-class">我是p元素</p><div>后面需要被改变的标签<div></div>//dom结构二<divclass="d-class"><pclass="p-class">我是p元素</p><h1>节点被更改<h1></div>观察上面的dom节点一和dom节点二,不难发现。其实只有div改变成了h1。但就是这一个小小的改动,浏览器重新渲染的话,不用虚拟dom的情况下浏览器会这么做↓
删除所有节点
重新创建div标签,给div标签赋予class
在div下重新创建p标签,给P标签增加class并填充文本
在div下重新创建h1标签并填充文本
观察三个步骤,其中最耗费性能的,就是创建节点的过程。
在使用虚拟DOM的情况下。更新视图会有如下操作↓
比对结构一和结构二的树结构
发现div标签没有变。直接拿来复用(这样就不用删除也不用重新创建了)
发现p标签也没有变动,可以直接拿来复用(不用重新删除并创建)
发现div标签变成了h1标签,删除原先div标签,重新创建h1标签并插入(只有这一个需要重新生成)
然后就明朗了。
在不使用虚拟DOM的情况下,哪怕改变的只有一个子节点,所有的节点也都需要重新渲染。(我们以前使用JQ的那个时代,就是这么干的)
在使用虚拟DOM的情况下。有很多没有改变的节点(或组件)可以被复用,直接省掉了好多创建节点的步骤。所以虚拟DOM会带来性能上的提升。
diff算法很简单,在虚拟DOM中,会有两棵树,一颗修改前的树,一颗修改后的树。
在react中,diff中有三大策略。
diff三大策略Treediff?(树比对)
比较新旧两棵树,找出哪些节点需要更新
发现组件则进入Componentdiff
发现节点则进入Elementdiff
Componentdiff(组件比对)
如果节点是组件,比对组件类型
类型不同直接替换(删除旧组件,创建新组件)
类型相同则只更新属性
然后深入组件进行Treediff(递归)
Elementdiff(元素比对)
如果节点是原生标签,则看标签名
标签名不同直接替换(删除旧元素,创建新元素)
标签名相同则只更新属性
然后深入标签做Treediff(递归)
比对会从Treediff(树比对开始),扫到组件进入Componentdiff,扫到元素则进入Elemenetdiff。这是一个递归查询的过程。
key是干啥的?这玩意是如何对项目进行优化的。首先,diff会对新旧两颗树进行同级比较。绝不会出现跨层比较的情况。
在某一层比对发现元素类型不一样之后,自该层往下节点会直接全部删除,不会再往下比对。
更有意思的是↓
当组件树发生上图这样的变化时,从一个人类的思维来说,应该是删除p元素,再把span元素移动到左边。
但机器不是这么理解的!!
在机器的理解上,diff算法会对以上例子做出以下动作
比对新旧节点数第一层的div是否被更改,没有改动,进入下一层比较
发现旧节点树下第一个元素是p。新节点树下的第一个节点是span(p比span)
删除p元素,创建span元素。
再进行比对,发现旧节点树第二个元素是span,新节点树没有第二个元素(span比undefined)
删除span元素
也就是说,如果你删除第一个元素,后面所有的元素都会被删除然后重新创建。
基于机器这种笨重的思维方式情况下。
key诞生了。
说白了。key是一种标记。
没有用key的情况下机器默认的算法是按顺序一个一个去比较(注意奥,是按元素顺序去比较)。只要对应顺序上的元素类型对不上,直接删除然后重建。
但是!!用了key则不一样。用了key机器会开启另一种比较算法。
当虚拟dom发现一个节点存在key以后。他就不会按顺序去比较旧节点树中相同位置的那个元素。而是会在旧树的同级元素中遍历寻找该key所标记的节点是否存在。是否可以复用。
不使用key的情况
如上图所示,在不使用key的情况下。diff算法会根据元素的位置去比对。如果你像上图那样删除了第一个元素的话,后面的元素都会因为比对不成功而全部被删除然后重新创建。
在使用key的情况下
在使用了key的情况下,会寻找匹配新旧节点树中是否有可以复用的元素。这样可以避免不必要的性能浪费。
总结首先,虚拟dom本质上就只是一个js对象而已
diff算法有三大策略,树比对(Treediff),组件比对(Componentdiff),元素比对(Elementdiff)
尽量使用key,在增删元素的时候可以提高性能
创建并渲染一个节点的流程相对耗费性能,所以需要虚拟dom来尽量减少节点的重新创建和删除
原文:/post/React源码分析4-深度理解diff算法
React 每次更新,都会通过 render 阶段中的 reconcileChildren 函数进行 diff 过程。这个过程是 React 名声远播的优化技术,对新的 ReactElement 内容与旧的 fiber 树进行对比,从而构建新的 fiber 树,将差异点放入更新队列,对真实 DOM 进行渲染。简单来说,diff 算法是为了以最低代价将旧的 fiber 树转换为新的 fiber 树。
经典的 diff 算法在处理树结构转换时的时间复杂度为 O(n^3),其中 n 是树中节点的个数。在处理包含 个节点的应用时,这种算法的性能将变得不可接受,需要进行优化。React 通过一系列策略,将 diff 算法的时间复杂度优化到了 O(n),实现了高效的更新 virtual DOM。
React 的 diff 算法优化主要基于以下三个策略:tree diff、component diff 和 element diff。tree diff 策略采用深度优先遍历,仅比较同一层级的元素。当元素跨层级移动时,React 会将它们视为独立的更新,而不是直接合并。
component diff 策略判断组件类型是否一致,不一致则直接替换整个节点。这虽然在某些情况下可能牺牲一些性能,但考虑到实际应用中类型不一致且内容完全一致的情况较少,这种做法有助于简化 diff 算法,保持平均性能。
element diff 策略通过 key 对元素进行比较,识别稳定的渲染元素。对于同层级元素的比较,存在插入、删除和移动三种操作。这种策略能够有效管理 DOM 更新,确保性能。
结合源码的 diff 整体流程从 reconcileChildren 函数开始,根据当前 fiber 的存在与否决定是直接渲染新的 ReactElement 内容还是与当前 fiber 进行 Diff。主要关注的函数是 reconcileChildFibers,其中的细节与具体参数的处理方式紧密相关。不同类型的 ReactElement(如 REACT_ELEMENT_TYPE、纯文本类型和数组类型)将走不同的 diff 流程,实现更高效、针对性的处理。
diff 流程结束后,形成新的 fiber 链表树,链表树上的 fiber 标记了插入、删除、更新等副作用。在完成 unitWork 阶段后,React 构建了一个 effectList 链表,记录了需要进行真实 DOM 更新的 fiber。在 commit 阶段,根据 effectList 进行真实的 DOM 更新。下一章将深入探讨 commit 阶段的详细内容。
2024-12-24 08:28
2024-12-24 07:55
2024-12-24 07:43
2024-12-24 07:43
2024-12-24 06:49