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