别慌, react diff 没有那么复杂前言 从前对各种底层原理都比较抵触,总觉得极其复杂,肯定不是自己这只菜鸟能搞定
前言
从前对各种底层原理都比较抵触,总觉得极其复杂,肯定不是自己这只菜鸟能搞定的。但随着技术的成长,再回头看这些,也逐渐明白了那些令我恐惧的东西是什么。
本文会以尽可能简单清晰的话来说清楚 react diff,希望能帮助到你。
为什么要做 diff
举个例子,在没有框架的情况下,当你需要这样的 html 变更时:
// dom结构 一
<div>
<p>我是p元素</p>
<div id="乱七八糟" class="xxxxx" (这里还有一堆属性)>后面需要被改变的标签<div>
</div>
// dom结构 二
<div>
<p>我是p元素</p>
<div>节点被更改<div>
</div>
正常的想法应该是删除 div 标签,再搞个新的 div 插进去
const oldDiv = document.getElementById('乱七八糟');
oldDiv.parentNode.removeChild(oldDiv); // 删除旧的 div
const newDiv = document.createElement('div');
newDiv.innerHTML = '节点被更改'; // 创建新的 div
oldDiv.parentNode.appendChild(newDiv); // 添加新的 div
但创建节点其实是很消耗性能的, 更好的做法应该是,删除/新增/修改掉前面的 div 的属性之类的,不过这样写起来会相当繁琐,我们通常会抽出来类似这样的方法:
function updateElement(oldElement, newElement) {
// 更新旧元素的内容
oldElement.innerHTML = newElement.innerHTML;
// 更新旧元素的属性
Array.from(newElement.attributes).forEach(attr => {
oldElement.setAttribute(attr.name, attr.value);
});
const oldAttributes = Array.from(oldElement.attributes);
oldAttributes.forEach(attr => {
if (!newElement.hasAttribute(attr.name)) {
oldElement.removeAttribute(attr.name);
}
});
}
这么一来,我们就可以比较方便的实现节点的更新了。但这只是说明了节点如何变更,但变更的前提应该是:节点如何比较?
这就引出了 diff 算法了,它的好处在于:
-
自动化:你只需要提供两棵虚拟 DOM 树,Diff 算法会自动比较它们,找出差异。
-
性能优化:React 的 diff 算法通过一些优化手段(如分层比较、利用 key 值等),提高了比较和更新的性能
diff 的过程是怎么样的
现在你知道了,diff 大概就是这样一个函数:传入两棵树,传出变更内容
function diff(nodeTree1, nodeTree2) {
return diffContent;
}
我们再来看一下 diff 具体的过程吧,首先对比会分为三种:
-
tree diff
,节点树对比- 逐层逐节点的对比,判断是否有节点出现差异
- 如果该节点是组件节点,进入 component diff
- 如果该节点是元素节点,进入 element diff
-
component diff
,组件对比- 组件名不同时,打删除,新增 tag
- 相同时,打更新 tag
- 递归组件子节点,进行 tree diff
-
element diff
,元素对比- 标签名不同时,打删除,新增 tag
- 相同时,打更新 tag
- 递归组件子节点,进行 tree diff
这就是简化版的 diff 算法核心了,看到这一步,你已经学会七七八八了, 让我们更进一步吧。
了解完这个核心,相信你也会提出问题:刚才似乎都是说一个子节点的情况,多个子节点要怎么和多个子节点进行对比呢?
a { b {c} }
对比a { b {d} }
,按上述原则很好解决,每层只有一个节点,不需要什么判断a { b c }
对比a { c b }
,涉及到了多节点的对比,这样怎么处理,难道 bc 和 cb 仅仅因为顺序不同就 diff 了吗,这不浪费吗
是的,这里其实没有太多技巧,diff 用的是最简单的按顺序逐一对比的方案
按照逐个对比来看:
- 新树中第一个为
b
,而旧树种第一个为c
,所以这个b
要删除,并且新增一个c
- 新树中第二个为
c
,而旧树种第一个为b
,所以这个c
要删除,并且新增一个b
- 结束
相信你也能看出来,这很浪费吧?所以 react 这里做了一些优化
react diff 的 key 优化
通过增加 key,react 解决了上述的问题,diff 会分为两次遍历
第一轮遍历
-
线性对比:React 会从头开始线性地比较新旧节点。
- 如果新旧节点类型和 key 都相同,则标记为更新节点。
- 如果新旧节点类型和 key 存在不同,则标记为删除旧节点并创建新节点。
-
结束条件:当发现新旧节点类型不同时,线性遍历会停止,进入第二轮遍历。
第二轮遍历
-
创建 Map:将旧树中剩余的节点放入一个 Map 中,
key
为节点的key
值,值为对应的节点。 -
查找复用节点:继续遍历新树中的剩余节点,尝试在 Map 中查找可复用的节点。
- 如果在 Map 中找到相同
key
的节点,则标记为更新节点,并从 Map 中删除该节点(因为已经复用了)。 - 如果找不到相同
key
的节点,则标记为创建新节点。
- 如果在 Map 中找到相同
-
处理剩余节点:遍历完成后,Map 中剩余的节点都是需要删除的旧节点。
具体例子
假设旧树是:
<ul>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
新树是:
<ul>
<li key="A">A</li>
<li key="C">C</li>
<li key="E">E</li>
</ul>
第一轮遍历
- 比较第一个节点
A
,发现相同,标记为更新。 - 比较第二个节点
B
和C
,发现不同,标记为删除旧节点B
并创建新节点C
。 - 继续比较,发现第三个节点
C
和E
类型不同,标记为删除旧节点C
并创建新节点E
。 - 继续比较,发现旧节点
D
没有对应的新节点,标记为删除。
第二轮遍历
-
将旧树中剩余的节点
B
、C
、D
放入 Map 中:Map = { B: <Fiber for B>, C: <Fiber for C>, D: <Fiber for D> } ```
-
继续遍历新树中的剩余节点:
-
找到
C
,在 Map 中找到相同key
的节点,标记为更新,并从 Map 中删除:Map = { B: <Fiber for B>, D: <Fiber for D> } ```
-
找到
E
,在 Map 中找不到相同key
的节点,标记为创建新节点。
-
-
最后,Map 中剩余的节点
B
和D
都标记为删除。
最后
恭喜,看到这里,你已经理解了一个 react diff 的核心过程了。如果这篇文章对你有帮助,就点个赞吧。
转载自:https://juejin.cn/post/7424009003922391050