大家好,我是小杜杜,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
所属的Component
REACT_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
是否在旧的中出现过,如果发现旧的中存在,然后判断是否去移动B
B
是否移动的条件为index < lastIndex,及在旧的Index
为1
,lastIndex
为0,所以并不满足条件,因此不会移动B
lastIndex
产生疑问,它到底是什么?实际上它是一个浮标,或者说是一个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
吧~