这是我参与「第四届青训营 」笔记创作活动的第18天
原理
由于频繁的操作 DOM 会产生性能问题,比如造成浏览器的回流和重绘。
通过 JS 对象模拟 DOM 中的结点,再通过特定的 render 方法将其渲染成真实的 DOM 结点。将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数。
虚拟 DOM 是一个 JS 对象,包含结点类型 tagName、属性 props、子节点 children 三个属性。
<div id="app">
<p class="text">test</p>
</div>
将上面的HTML元素转化为虚拟DOM:
{
tagName: 'div',
props: {
id: 'app'
},
chidren: [
{
tagName: 'p',
props: {
className: 'text'
},
chidren: [
'test'
]
}
]
}
如何实现上述转化呢?Virtual DOM 算法有以下步骤:
-
用
JavaScript对象结构表示DOM树的结构;- 用这个树构建一个真正的
DOM树,插到文档当中
- 用这个树构建一个真正的
-
当状态变更的时候,重新渲染一棵对象树。然后用新的对象树和旧的对象树进行比较,记录两棵树差异(Diff算法)
-
tree diff:两棵树只会对同一层级的节点进行比较,对不同层级的节点只有创建和删除。
因此只会对相同颜色方框内的
DOM节点进行比较,即同一个父节点下的所有子节点。如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。如果是不同层级,并不会出现移动操作,而是重新创建。
当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。React diff 的执行情况:create A -> create B -> create C -> delete A。
-
component diff:如果组件的
class一致,则模式为相似的树结构;否则认为是不同树结构,从而替换整个组件下的所有子节点当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。
-
element diff:对于同一层级的一组子节点,它们可以通过唯一
id进行区分- 插入:新的
component类型不在老集合里,即全新的节点,需要对新节点执行插入操作。 - 移动:新的
component类型在老集合里,且element是可更新的类型,执行移动操作,复用以前的DOM节点 - 删除:新集合没有老的
component,执行删除操作。
- 插入:新的
-
-
把步骤 3 所记录的差异应用到步骤 2 所构建的真正的
DOM树上,视图就更新了
优化
新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。
对同一层级的同组子节点,添加唯一 key 进行区分。 此时 React 给出的 diff 结果为,B、D 不做任何操作,A、C 进行移动操作,即可。
注:不要用数组的 index 作为唯一 key。