React原理
在某一时间节点调用 React 的 render()
方法,会创建一棵由 React 元素组成的树(虚拟DOM)。
在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。
React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM。
什么是虚拟DOM?——就是一个JS对象,用来描述真实的DOM。
更多学习:查看我的文章《React 中的Virtual DOM》
通过 JS 来模拟 DOM(虚拟dom):
const ul = {
tag: 'ul',
props: {
class: 'list'
},
children: {
tag: 'li',
children: '1'
}
}
上述代码对应的 DOM 就是
<ul class='list'>
<li>1</li>
</ul>
Diffing 算法
即对两个虚拟DOM做比对时的算法。同级比较。
当对比两颗树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。
1. 比对不同类型的React元素
当根节点为不同类型的元素时,React 会卸载原有的树并且建立起新的树。
- 当卸载一颗树时,对应的 DOM 节点也会被销毁。
- 当建立一颗新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。
- (在根节点以下的组件也会被卸载,它们的状态会被销毁。)
<div>
<Counter />
</div>
<span>
<Counter />
</span>
2. 比对同一类型的React元素
React 会保留 DOM 节点,仅比对及更新有改变的属性。
<div className="before" title="stuff" />
<div className="after" title="stuff" />
3. 比对同类型的组件元素
当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。
React 将更新该组件实例的 props
以跟最新的元素保持一致。
下一步,调用 render()
方法,diff 算法将在之前的结果以及新的结果中进行递归。
对子节点进行递归
keys
组件实例是基于它们的 key 来决定是否更新以及复用。
能不用index
做key
值就不要用!
案例分析:
const list = [{ id: 0, val: "a" }, { id: 1, val: "b" }, { id: 2, val: "c" }];
用index做key值的问题
react生成虚拟DOM并渲染真实DOM。
遍历渲染值为val: a b c
key值为index: 0 1 2
list删除第一项,并setState(),则react生成新的虚拟DOM,并与老的虚拟DOM比对,发现第key=0的值变成了b,key=1的值变成了c,key=2的项没有了。则生成真实DOM时是分别修改值key为0、1的值等。
遍历渲染值为val: b c
key值对应为id: 0 1
用表内的唯一值做key【非常推荐】
react生成虚拟DOM并渲染真实DOM。
遍历渲染值为val: a b c
key值对应为id: 0 1 2
list删除第一项,并setState(),则react生成新的虚拟DOM,并与老的虚拟DOM比对,发现是key为0的项没有了,则生成真实DOM时删除key=0第一个节点即可。
遍历渲染值为val: b c
key值对应为id: 1 2
注意
1.该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
2.Key 应该具有稳定,可预测,以及列表内唯一的特质。
更多请查看官方文档《协调》
其他文章摘抄
首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为了两步:
- 树的深度遍历:首先从上至下,从左往右遍历对象。这一步中会给每个节点添加索引,便于最后渲染差异
- 一旦节点有子元素,就去判断子元素是否有不同。
在第一步算法中我们需要判断新旧节点的 tagName
是否相同,如果不相同的话就代表节点被替换了。如果没有更改 tagName
的话,就需要判断是否有子元素,有的话就进行第二步算法。
在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。
查看原文部分《Virtual DOM》
关于React 的深度优先遍历 推荐阅读《深入理解react中的虚拟DOM、diff算法》
摘抄(需整理)
强烈推荐:React 源码剖析系列 - 不可思议的 react diff
- react官方建议不要进行DOM节点跨层级操作
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。【重点】当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。
竹子为什么会选择vue而没有选择react:
- react的diff算法不合适:编辑器内会有频繁的拖拽、移动元素的操作,比如位于根节点下的一个图片组件A被拖放到很深层的一个节点内。但是react只会简单地考虑同层级节点的位置移动,对于不同层级的节点,react只会进行删除和新建的操作。当根节点发现子节点中组件A消失了,就会直接销毁A;当深层节点发现多了子节点A,则会创建新的A(包括A内的所有子节点)作为其子节点。
所以:当出现节点跨层级移动时,并不会出现想像中的移动操作,而是以A为根节点的树先被删除再被重新创建。这是一种影响react性能的操作,因此react官方建议不要进行DOM节点跨层级操作。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。
- 如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型!
如果两个 component 是不同类型但结构相似时,React diff 会影响性能,因为该算法不会尝试匹配不同组件类型的子树。
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入,新建)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
移动:可以复用以前的 DOM 节点
写博客的原因或好处:
分享我的技术心得;自己做技术总结;好记性不如烂笔头,方便自己快速查阅和回顾。
content1:
<div>
菜菜
<article>
<h1>标题</h1>
</article>
</div>
content2:
<div>
石头
+ <p>满足条件时插入一个p标签</p>
<article>
<h1>标题</h1>
</article>
</div>
content3:
<div>
石头
<p>满足条件时插入一个p标签</p>
<div>
<article>
<h1>标题</h1>
</article>
</div>
</div>
content4:
// (react非常不建议这样写,所以平时基本上不会遇到这种情况去对比)
<section>
石头
<p>插入一个p标签</p>
<article>
<h1>标题</h1>
</article>
</section>
content5:
<header>变成了头部</header>
// 虚拟DOM
const content1 = {
tagName: 'div',
props: {},
children: [
'菜菜',
{
tagName: 'article',
props: {},
children: [
{ tagName: 'h1', props: {}, children: ['标题'] }
]
}
]
}
const content2 = {
tagName: 'div',
props: {},
children: [
'石头',
{
tagName: 'p',
props: {},
children: [
'满足条件时插入一个p标签'
]
},
{
tagName: 'article',
props: {},
children: [
{ tagName: 'h1', props: {}, children: ['标题'] }
]
}
]
}
// 1. 比对不同类型的React元素(父节点tagName)
// 2. 比对同一类型的React元素
// 3. 比对同类型的组件元素
const content3 = {
tagName: 'div',
props: {},
children: [
'石头',
{
tagName: 'p',
props: {},
children: [
'满足条件时插入一个p标签'
]
},
{
tagName:'div',// article 包了一层div
props:{},
children:[
{
tagName: 'article',
props: {},
children: [
{ tagName: 'h1', props: {}, children: ['标题'] }
]
}
]
}
]
}
const content4 = {
tagName: 'section',
props: {},
children: [] // children 同content2
}
const content5 = {
tagName: 'header',
props: {},
children: ['变成了头部']
}