VirtualDOM
[[toc]]
React 的核心思想
给我一个数据,我根据这个数据生成一个全新的Virtual DOM
,然后跟我上一次生成的Virtual DOM去 diff
,得到一个Patch
,
然后把这个Patch打到浏览器的DOM上去。完事,并且这里的patch显然不是完整的虚拟DOM
,
而是新的虚拟DOM和上一次的虚拟DOM经过diff
后的差异化
的部分。
JSX和createElement
我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX
编写:
class Hello extends Component { |
第二种是直接使用React.createElement
编写:
class Hello extends Component { |
实际上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, ...children)
方法提供的语法糖。也就是说所有的JSX代码最后都会转换成React.createElement(...)
,Babel
帮助我们完成了这个转换的过程。
注意:babel在编译时会判断JSX中组件的首字母,当首字母为小写时
,其被认定为原生DOM标签
,createElement的第一个变量被编译为字符串;当首字母为大写时
,其被认定为自定义组件
,createElement的第一个变量被编译为对象
,所以组件首字母要大写
Virtual DOM
冷静对待虚拟dom,他不是一定能够提升页面的性能,如果是首次渲染,Vitrua lDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存,是因为有diff他才会展现它的优势
Virtual DOM的存在的意义
- Vitrua Dom为React带来了跨平台渲染的能力。以React Native为例子;React根据Vitrual Dom画出相应平台的ui层,只不过不同平台画的姿势不同而已
- 服务端渲染
- 函数式编程
Virtual DOM 基本步骤:
- 用
js对象来表示DOM树的结构
; 然后用这个树构建一个真正的DOM树,插入到文档中。 - 当状态变更的时候,
重新构造一个新的对象
,然后用这个新的树和旧的树作对比,记录两个树的差异
。 - 把2所记录的差异应用在步骤1所构建的真正的DOM树上,视图就更新了。
看看虚拟DOM的真实模样
type
:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)key
:组件的唯一标识,用于Diff算法ref
:用于访问原生dom节点props
:传入组件的propsowner
:当前正在构建的Component所属的Component$$typeof
:防止xss攻击,如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能为你的应用程序带来风险。JSON中不能存储Symbol类型的变量,而React渲染时会把没有$$typeof
标识的组件过滤掉。self
指定当前位于哪个组件实例。_source
指定调试代码来自的文件(fileName)和代码行数(lineNumber)。
简单实现 vdom
<body> |
class React { |
然后根据不同的情况,来进行树上节点的增删改的操作。这个过程是分为diff和patch:
- diff:递归对比两棵 VDom 树的、对应位置的节点差异
- patch:根据不同的差异,进行节点的更新
diff
其实React的 virtual dom的性能好也离不开它本身特殊的diff算法。传统的diff算法时间复杂度达到O(n3),而react的diff算法时间复杂度只是o(n),react的diff能减少到o(n)依靠的是react diff的三大策略。
传统diff 对比 react diff
传统的diff算法追求的是“完全
”以及“最小
”,而react diff则是放弃了这两种追求:
在传统的diff算法下,对比前后两个节点,如果发现节点改变了,会继续去比较节点的子节点,一层一层去对比
。就这样循环递归去进行对比,复杂度就达到了O(n3),n是树的节点数,想象一下如果这棵树有1000个节点,我们得执行上十亿次比较,这种量级的对比次数,时间基本要用秒来做计数单位了。
React diff 三大策略
- tree diff:Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
(DOM结构发生改变-----直接卸载并重新creat)
- component diff:组件的DOM结构一样—–不会卸载,但是会update
- element diff:所有同一层级的子节点.他们都可以通过key来区分—–同时遵循1.2两点
虚拟DOM树分层比较(tree diff
)
上图中,div只会和同一层级的div对比,第二层级的只会和第二层级对比。 这样算法复杂度就可以达到O(n)。
但是如果DOM节点出现了跨层级操作,diff会如何处理?
React是不会机智的判断出子树仅仅是发生了移动,而是会直接销毁,并重新创建这个子树,然后再挂在到目标DOM上;
实际上,React官方也并不推荐我们做出跨层级的骚操作。所以我们可以从中悟出一个道理:就是我们自己在实现组件的时候,一个稳定的DOM结构是有助于我们的性能提升的。
组件间的比较(component diff
)
核心的策略还是看结构是否发生改变。React是基于组件构建应用的,对于组件间的比较所采用的策略也是非常简洁和高效的。
如果是同一个类型的组件,则按照原策略进行Virtual DOM比较。
如果不是同一类型的组件,则将其判断为dirty component,从而替换整个组价下的所有子节点。
如果是同一个类型的组件,有可能经过一轮Virtual DOM比较下来,并没有发生变化。如果我们能够提前确切知道这一点,那么就可以省下大量的diff运算时间。因此,React允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析。
// 对比自定义组件 |
元素间的比较(element diff
)
当节点处于同一层级的时候,react diff 提供了三种节点操作:插入、删除、移动。
操作| 描述|
–|:–:|:–:|–|
插入|新节点不存在于老集合当中,即全新的节点,就会执行插入操作|
移动|新节点在老集合中存在,并且只做了位置上的更新,就会复用之前的节点,做移动操作(依赖于Key)|
删除|新节点在老集合中存在,但节点做出了更改不能直接复用,做出删除操作|
Key的作用
react利用key来识别组件,它是一种身份标识标识,就像我们的身份证用来辨识一个人一样。每个key对应一个组件,相同的key react认为是同一个组件,这样后续相同的key对应组件都不会被创建。
key的使用场景
- 数组动态创建的子组件
- 为一个有复杂繁琐逻辑的组件添加key后,后续操作可以改变该组件的key属性值,从而达到先销毁之前的组件,再重新创建该组件。
我们在循环渲染列表时候(map)时候忘记标记key值报的警告,既然是警告,就说明即使没有key的情况下也不会影响程序执行的正确性,其实这个key的存在只会影响diff算法的复杂度
(不是一定会提高性能),也就是说你不加上Key就会暴力渲染,加了Key之后,React就可以做出移动的操作了,看例子:
每个节点都加上了唯一的key值,通过这个Key值发现新老集合里面其实全部都是相同的元素,只不过位置发生了改变。因此就无需进行节点的插入、删除等操作了,只需要将老集合当中节点的位置进行移动就可以了。React给出的diff结果为:B、D不做操作,A、C进行移动操作
。react是如何判断谁该移动,谁该不动的呢?
react会去循环整个新的集合:
① 从新集合中取到B
,然后去旧集合中判断是否存在相同的B
,确认B
存在后,再去判断是否要移动:B
在旧集合中的index = 1
,有一个游标叫做lastindex
。默认lastindex = 0
,然后会把旧集合的index和游标作
对比来判断是否需要移动,如果index < lastindex ,那么就做移动操作,在这里B的index = 1
,不满足于 index < lastindex
,所以就不做移动操作,然后游标lastindex更新,取(index, lastindex) 的较大值
,这里就是lastindex = 1
② 然后遍历到A
,A
在老集合中的index = 0
,此时的游标lastindex = 1
,满足index < lastinde
x,所以对A需要移动到对应的位置,此时lastindex = max(index, lastindex) = 1
③ 然后遍历到D
,D
在老集合中的index = 3
,此时游标lastindex = 1
,不满足index < lastindex
,所以D保持不动。lastindex = max(index, lastindex) = 3
④ 然后遍历到C
,C
在老集合中的index = 2
,此时游标lastindex = 3
,满足 index < lastindex
,所以C移动到对应位置。C之后没有节点了,diff就结束了
以上主要分析新老集合中节点相同但位置不同
的情景,仅对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?
和第一种情景基本是一致的,react还是去循环整个新的集合:
① 不赘述了,和上面的第一步是一样的,B不做移动,lastindex = 1
② 新集合取得E
,发现旧集合中不存在,则创建E并放在新集合对应的位置,lastindex = 1
③ 遍历到C
,不满足index < lastindex
,C
不动,lastindex = 2
④ 遍历到A
,满足index < lastindex
,A
移动到对应位置,lastindex = 2
⑤ 当完成新集合中所有节点 diff
时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D
,因此删除节点 D
,到此 diff 全部完成
但是 react diff也存在一些问题,和需要优化的地方,看下面的例子:
在上面的这个例子,A、B、C、D都没有变化,仅仅是D
的位置发生了改变。看上面的图我们就知道react并没有把D的位置移动到头部,而是把 A、B、C分别移动到D
的后面了,通过前面的两个例子,我们也大概知道,为什么会发生这样的情况了:
因为D
节点在老集合里面的index
是最大的,使得A、B、C三个节点都会 index < lastindex
,从而导致A、B、C都会去做移动操作。所以在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
三句箴言
所以经过这么一分析react diff
的三大策略,我们能够在开发中更加进一步的提高react的渲染效率。
- 在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
- 使用
shouldComponentUpdate()
方法节省diff的开销 - 在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
为什么不推荐使用index作为Key
看下面这个示例
import React, { Component } from 'react'; |
我们在三个输入框里面,依次输入1,2,3,点击Reverse按钮,按照我们的预期,这时候页面应该渲染成3,2,1,但是实际上,顺序依然还是1,2,3,证明数据确实是更新了的。那么为什么会发生这种事情,我们可以分析一下:
出现这种情况,使用key是用来表示唯一的标识组件,当发现setState前后key的值没有发生变化 ,react就会认为你setState前后是同一个组件,进而只会对内部的属性进行修改:
- 检测key值发现都是0,判定组件为同一个。
- 检测item.val部分,发现有变化重新渲染这部分
- 检测input,发现不依赖props,所以不进行重新渲染
diff 源码
// diff函数,对比两颗树 |
patch
因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。
所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。
差异类型
DOM操作可能会:
- 替换原来的节点,如把上面的div换成了section。
- 移动、删除、新增子节点, 例如上面div的子节点,把p和ul顺序互换。
- 修改了节点的属性。
- 对于文本节点,文本内容可能会改变。
所以,我们可以定义下面的几种类型:
var REPLACE = 0; |
patch 源码
function patch (node, patches) { |
applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:
function applyPatches (node, currentPatches) { |
总结
virtual DOM算法主要实现上面步骤的三个函数: react.createElement
、diff
、patch
,然后就可以实际的进行使用了。
// 1. 构建虚拟DOM |
当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。
参考文档
https://juejin.im/post/5cb66fdaf265da0384128445
https://blog.csdn.net/qq_36407875/article/details/84965311
https://www.cnblogs.com/zhuzhenwei918/p/7271305.html
http://react-china.org/t/react-react/26788
https://github.com/MuYunyun/blog/blob/master/React/%E4%BB%8E0%E5%88%B01%E5%AE%9E%E7%8E%B0React/4.diff%E7%AE%97%E6%B3%95.md