大家好,我是小杜杜,React中的虚拟DOM和diff算法是非常核心的特型,了解它们是非常有必要,只有了解,才能深入。
我们直接来看看以下几个问题:
虚拟DOM到底是什么,它与真实的DOM有什么不同?React中,为什么自定义组件的首字母要大写?虚拟DOM,性能就一定能够得到提升吗?diff算法与传统的diff算法有什么区别?为什么受到吹捧?diff策略有哪些?它们是如何比较的?key值呢?如果你对上述问题有疑问,那么这篇文章一定能够帮助到你~
跟之前一样先附上这篇的知识图,还请各位多多支持:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gi7s86Es-1658297236093)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f82868a39244b3281538fb9bf4c7103~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
我们首先用React.createElement和document.createElement创建以下,然后进行打印,看一下,虚拟DOM和真实DOM有什么区别:
const VDOM = React.createElement('div', {}, '小杜杜')
const DOM = document.createElement("div");
DOM.innerHTML = '小杜杜'
console.log(`虚拟DOM:`, VDOM)
console.log(`真实DOM:`, DOM)
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9o1tAxg-1658297236093)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58db2ee32bde44b3afd5e2e6af3e3647~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
我们可以看出虚拟DOM是一个对象的结构,而真实的DOM是一个dom的结构,而这个dom结构究竟是什么呢?我们可以通过断点去看看: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjLhjDST-1658297236093)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c859397a03024903939f934907052cc2~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
我们可以看到,在真实的DOM上,默认会挂载很多属性和方法,但在实际中,我们并不需要去关心这些属性和方法(注意:这些属性和方法是默认的,因为标准是这么设计的)
所以从结构上来看:虚拟DOM要比真实DOM轻很多
假设我们有以下列表:
- 1
- 2
- 3
我们现在要将 1、2、3 替换为 4,5,6,7,我们直接操纵节点该如何处理?
innerHTML直接覆盖单纯操作来讲,第三种无疑是最方便的,第一种明显复杂一点,但从性能上来讲,第三种的性能最高,因为存在重排与重绘的问题,我们知道浏览器处理DOM是很慢的,如果页面比较复杂,频繁的操做DOM会造成很大的开销。
所以在原生的DOM中我们要想性能高,就只能选择第一种方案,但这样明显给我们带来了复杂度,不利于目前的开发(会在下文详细讲到~)
在传统的Web应用中,数据的变化会实时地更新到用户界面中,于是每次数据微小的变化都会引起DOM的渲染。
而虚拟DOM的目:是将所有的操作聚集到一块,计算出所有的变化后,统一更新一次虚拟DOM
也就是说,一个页面如果有500次变化,没有虚拟DOM的就会渲染500次,而虚拟DOM只需要渲染一次,从这点上来看,页面越复杂,虚拟DOM的优势越大
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2aFQ9Xs-1658297236094)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b16d46da6c7a44bfa1b3acfc2595a43b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
在上面我们说过虚拟DOM实际上就是对象,接下来详细看看这个对象有什么,栗子🌰:
我是小杜杜
- React
- Vue
转化后:
{
type: 'div',
props: { class: 'Index' },
children: [
{
type: 'div',
children: '我是小杜杜'
},
{
type: 'ul',
children: [
{
type: 'li',
children: 'React'
},
{
type: 'li',
children: 'Vue'
},
]
}
]
}
主要转化为:
key和ref,会形成单独的key名)从结构上来说,虚拟DOM并没有真实DOM哪些乱七八糟的东西,因此,我们就算直接把虚拟DOM删除后,重新建一个也是非常快的
作为一个前端人,多多少少都知道React的核心是JSX语法,说白了,JSX就是JS上的扩展,就像一个拥有javascript全部功能的模板语言
我们写的代码最终是要呈现在浏览器上,浏览器会识别你的代码是React吗?很显然,浏览器并不知道你的代码是React,更不会识别JSX了,实际上浏览器对ES6的一些语法都识别不了,要想让浏览器识别,就需要借助Babel
要通过Babel去对JSX进行转化为对应的JS对象,才能让浏览器识别,此时就会有个依据去判断是原生DOM标签,还是React组件,而这个依据就是标签的首字母
如果标签的首字母是小写,就会被认定为原生标签,反之就是React组件
举个栗子🌰:
class Info extends React.Component {
render(){
return(
Hi!我是小杜杜
欢迎
我是子组件
)
}
}
上述代码会被翻译为:
class Info extends React.Component {
render(){
return React.createElement(
'div',
null,
"Hi!我是小杜杜",
React.createElement('p', null, '欢迎'), // 原生标签
React.createElement(
Children, //自定义组件
null, // 属性
'我是子组件' //child文本内容
)
)
}
}
换言之,我们的JSX结构最终会被翻译为React.createElement的结构,那么为什么要使用JSX而不用 createElement书写呢?
其实这两种写法都是可以的,但JSX形式明显要比createElement方便很多。
综上所诉,在React中,组件大写的原因是Babel进行转化,需要一个条件去判断是原生标签还是自定义组件,通过首字母的大小写去判断
在这里,额外说一下React.Fragment这个组件,熟悉React的小伙伴应该知道,在React中,组件是不允许返回多个节点的,如:
return 我是小杜杜
React
Vue
我们想要解决这种情况需要给为此套一个容器元素,如
return
我是小杜杜
React
Vue
但这样做,无疑会多增加一个节点,所以在16.0后,官方推出了Fragment碎片概念,能够让一个组件返回多个元素,React.Fragment 等价于<>>
return
我是小杜杜
React
Vue
可以看到React.Fragment实际上是没有节点的 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFoakHHK-1658297236094)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2b290182958b45ba81ce6441cbb405f5~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)] 那么这个特殊的组件,会被createElement翻译的不一样吗?
其实是一样的,还是会被翻译为React.createElement(React.Fragment, null, "")这样的形式,这点要注意
同时在React也支持返回数组的形式,如:
[1,2,3].map(item=>{item}
)
实际上这种会被React的底层进行处理,默认会加入Fragment,也就是等价于
1
2
3
我们知道
等价于<>>,那么他们有不同吗?
在上述讲过,key和ref会被单独存放,ref不用考虑,在循环数组时,我们必须要有key,实际上允许有key的,而<>>无法附上key,所以这是两者的差距
使用原生JS的时候,我们需要的关注点在操作DOM上,而React会通过虚拟DOM来确保DOM的匹配,也就是说,我们关注的点不在时如何操作DOM,怎样更新DOM,React会将这一切处理好
此时,我们更加关注于业务逻辑,从而提高开发效率
经过之前的讲解,我们发现
虚拟DOM优势明显强于真实的DOM,我们来看看虚拟DOM如何工作的?
实际上,React会将整个DOM保存为虚拟DOM,如果有更新,都会维护两个虚拟DOM,以此来比较之前的状态和当前的状态,并会确定哪些状态被修改,然后将这些变化更新到实际DOM上,一旦真正的DOM发生改变,也会更新UI
要牢记一句话:浏览器在处理DOM的时候会很慢,处理JavaScript会很快
所以在虚拟DOM感受到变化的时候,只会更新局部,而非整体。同时,虚拟DOM会减少了非常多的DOM操作 ,所以性能会提升很多
通过上面的理解,很多人认为虚拟DOM一定会提高性能,一定会更快,其实这个说法有点片面,因为虚拟DOM虽然会减少DOM操作,但也无法避免DOM操作
它的优势是在于diff算法和批量处理策略,将所有的DOM操作搜集起来,一次性去改变真实的DOM,但在首次渲染上,虚拟DOM会多了一层计算,消耗一些性能,所以有可能会比html渲染的要慢
注意,虚拟DOM实际上是给我们找了一条最短,最近的路径,并不是说比DOM操作的更快,而是路径最简单
就好比条条大路通罗马,虽然走的方向不同,但最终到达的目的地都是相通的,不同的路径对应的时间不同,虚拟DOM就是规划出最短的路径,但最终还是需要人(真实DOM)去走的(有不对的地方,欢迎评论区讨论~)
React具有超强的兼容性,可分为:浏览器的兼容和跨平台兼容
React基于虚拟DOM实现了一套自己的事件机制,并且模拟了事件冒泡和捕获的过程,采取事件代理、批量更新等方法,从而磨平了各个浏览器的事件兼容性问题React和React Native都是根据虚拟DOM画出相应平台的UI层,只不过不同的平台画法不同而已我们构建的JSX代码会被转为React.createElement的形式,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TbJrC11f-1658297236094)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58db2ee32bde44b3afd5e2e6af3e3647~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
React.createElement:它的功能是将props和子元素进行处理后返回一个ReactElement对象(key和ref会特殊处理)
ReactElement这个对象会将传入的几个属性进行组合并返回
key和ref,会形成单独的key名)key和ref,会形成单独的key名)Diff算法dom节点Component所属的ComponentREACT_ELEMENT_TYP,可以防止XXSXSS攻击(跨站脚本攻击):通常指的是通过利用发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。
React自身可以预防XSS,主要依靠的就是 ?typeof
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7;
从上述代码我们知道?typeof实际上是Symbol类型,当然Symbol是ES6的,如果环境不支持ES6,?typeof会被赋值于 0xeac7
那么这个变量为什么可以预防XSS呢?
简单的说,用户存储的JSON对象可以是任意的字符串,这可能会带来潜在的危险,而JSON对象不能存储于Symbol类型的变量,React 可以在渲染的时候把没有?type 标识的组件过滤掉,从而达到预防XSS的功能
虚拟DOM转化为真实DOM的这个过程实际上非常复杂,大体上可以分为四步: 处理参数、批量处理、生成html和渲染html
ReactDOM.render(element, container[, callback])将组件进行渲染,这里会判断是原生标签还是React自定义组件DOM标签、props进行处理,并根据对应的标签类型创造对应的DOM节点,利用updateDOMProperties将props插入到DOM节点,最后渲染到上面经过上面的讲解,我们知道React会维护两个虚拟DOM,那么是如何来比较,如何来判断,做出最优的解呢?这就用到了diff算法
在React中,最值得夸赞的地方就是虚拟DOM与diff算法的结合,发展至今,个人认为React的diff算法远比传统的diff算法出名很多,那么原因究竟是什么呢?
React中的diff算法并非首创,而是引入,React团队为diff算法做出了质的优化,举个🌰
在计算一颗树转化为另一颗树有哪些改变时,传统的diff算法通过循环递归对节点进行依此对比,其算法复杂度达到了O(n^ 3),也就是说,如果展示 一千个节点,就要计算十亿次
再来看看React中的diff算法,算法复杂度为O(n),如果展示一千个节点,就要计算一千次
从十亿次更新到一千次,这可不是一点点的优化,而是非常巨大的优化,真心的佩服
那么,如何将O(n^ 3) 转化为O(n) 呢?
React通过三大策略完成了优化:
分别对应:tree diff、component diff、element diff
tree diff: 同级比较,既然DOM 节点跨层级的移动操作少到可以忽略不计,那么React通过updateDepth 对 Virtual DOM 树进行层级控制,也就是同一层,在对比的过程中,如果发现节点不在了,会完全删除不会对其他地方进行比较,这样只需要对树遍历一次就OK了
栗子🌰:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohbwzd2P-1658297236095)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/79baab17d9a14df6b30c07f2e0e7ed11~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
L 带着B和C从A的下面,跑到了R的下面,按理说应该把L移到R的下方,但这样会牵扯到跨层级比较,有可能在层级上移动的非常多,导致时间复杂度陡然上升注意:保持DOM的稳定会有助于性能的提升,合理的利用显示和隐藏效果会更好,而不是真正的删除或增加DOM节点
component diff:组件比较,React对于组件的策略有两种方式,一种是相同类型的组件和不同类型的组件
React会直接判定该组件为dirty component(脏组件),无论结构是否相似,只要判断为脏组件就会直接替换整个组件的所有节点举个栗子🌰:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQBiFWgH-1658297236095)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94d5d62774064dda8ef7abd8b873dd78~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
在比较时发现D => G,虽然两个组件的结构非常相似,React判断这两个组件并不是同一个组件(dirty component),就会直接删除 D,重新构建 G,在实际中,两个组件不同,但结构又非常相似,这样的情况会很少的
element diff:节点比较,对于同一层级的一子自节点,通过唯一的key进行比较
当所有节点处以同一层级时,React 提供了三种节点操作:插入(INSERT_MARKUP)、移动(MOVE_EXISTING)、删除(REMOVE_NODE)
component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。如:C 不在集合A、B中需要插入
component 类型,且element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点如:当组件D在集合 A、B、C、D中,且集合更新时,D没有发生更新,只是位置发生了改变,如:A、D、B、C,D的位置有4变换到了2
如果是传统的diff,会让旧集合的第二个B和新集合的D做比较,删除第二个B,在插入D
React中的diff并不会这么做,而是通过key来进行直接移动
component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。如: 组件D在集合 A、B、C、D中,如果集合变成了 新的集合A、B、C,D就需要删除
如果D的节点发生改变,不能复用和更新,此时会删除旧的D,再创建新的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNvOKPUy-1658297236095)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f8db39f510b645e48e308a3fb8088fbd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
顺序:
React会判断(新中)第一个B是否在旧的中出现过,如果发现旧的中存在,然后判断是否去移动BB是否移动的条件为index < lastIndex,及在旧的Index为1,lastIndex为0,所以并不满足条件,因此不会移动BlastIndex产生疑问,它到底是什么?实际上它是一个浮标,或者说是一个map的索引,一开始是默认的0,当每次比较后,会改变对应的值,也就是 lastIndex=(index, lastIndex)中的最大值,对第一步来说,就是lastIndex=(1, 0) => lastIndex为1A的比较,在旧的中A的index为0,lastIndex为1,满足index < lastIndex,因此对A进行移动,lastIndex还是为1D,index为3,lastIndex为1,D不移动,并且lastIndex为3C,index为2,lastIndex为3,C移动,lastIndex不变,此时操作结束[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQ3JRfOj-1658297236095)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a6946c01f3142948e2a7cced8d680dc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
顺序:
B与上述讲的一样,不移动,lastIndex为1E时,发现在旧的中并没有E这个节点,所以此时会建立,此时的lastIndex还是为1C中,index 为 2,lastIndex为 1,所以此时不满足index < lastIndex,故C不移动,lastIndex更新为 24.A同理,A移动,lastIndex不更新,为2 5. 在新集合遍历完毕中,发现并没有D这个节点,所以会删除D,操作结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrPRSv3X-1658297236096)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94dc4be27a4e46a59de6d5d9382639f7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
我们来看看这种情况,如果将D移入到第一个,我们发现lastIndex为 3,之后在进行比较,发现lastIndex都大于index,所以剩下的节点都会移动,所以在开发的过程中应该尽量减少节点移入首部的操作,会影响其性能
我们知道,在我们进行循环的时候要加入
key,那么key为什么说不能使用索引做为key值呢?有的时候在面试中也会问到,你在项目中key是如何设置的?为什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qcSoT9Q-1658297236096)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3732b5b8eb04748a488567954242f23~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
我们发现,当我们判断第一个B时,由于此时的key为0在旧的中key为0是A,B和A明显不是一个组件,所以会删除重建
所以无论是删除还是新增,或是移动,都会进行重新建立,这种方式与是否有key根本无关
这种方式于上面的一样,因为每一个节点都找不到对应的key,导致所有的节点都不能复用,都会重新创建,所以不能
只有通过唯一值,才能做到每一个节点都做到了复用,真正起到了diff算法的作用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvY7Bf06-1658297236096)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a6946c01f3142948e2a7cced8d680dc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?)]
虚拟DOM和diff算法是React中比较核心的,也是面试中比较常见的,在网上找了许多资料,整理学习,在这里面牵扯到一些React事件机制的问题,之后会专门做一章进行总结,还请多多关注~
说实话,写这种硬文真的有点累,而且花费的时间也较长,但如果你耐心看下去,一定会让你受益良多的,
【点赞】+【收藏】=【学会了】,还请各位小伙伴多多支持,后续还会有React的硬文,关注我,一起上车学习React吧~