著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
介绍
本篇对照源码和jsx模版简单介绍React Fiber子节点的diff算法。
JSX 模版
情况1:
假如构建这样的树: li-a对应正文1.2顺序比较key和type都相同
<ul>
<li key="a">1</li>
<li key="b">2</li>
</ul>
更改元素的类型后:p-b对应1.2.2key相同type不同
<ul>
<li key="a">1</li>
<p key="b">2</p>
</ul>
情况2: 调换li元素的位置。第一个li对应正文1.2顺序比较key不同,然后直接跳到正文4。
<ul>
<li key="a">1</li>
<li key="b">2</li>
</ul>
<ul>
<li key="b">2</li>
<li key="a">1</li>
</ul>
情况3: 对应正文1.1
假如构建这样的树:
const [hid, setHid] = useState(true)
<ul>
<li key="a">1</li>
{hid} ? null : <li key="b">2</li> // 结果为null
<li key="c">3</li>
</ul>
这个模版被创建后得到还没有创建fiber的虚拟dom li
相当于jsx: [a, null, c] , 下标index: 0,1,2。
reconcileChildrenArray中循环创建了fiber后: [a, c], 下标index: 0,2。
因为jsx存在了“空洞”,从而fiber.index出现了“跳跃”。
到setHid(false)更新了hid,
<ul>
<li key="a">1</li>
{hid} ? null : <li key="b">2</li> // 结果为2
<li key="c">3</li>
</ul>
reconcileChildrenArray第一阶段的for能够触发if (oldFiber.index > newIdx) {分支。因为旧树index的“跳跃”oldFiber.index = 2时,newIdx = 1。
问:旧fiber怎么会是空槽呢?空.sibling报错的,没办法用sibling获取兄弟节点。并且新树渲染后变成旧树,新树不会创建null的fiber,新树不会有空槽,所以旧树也不会有。假如真的存在空槽,那么oldFiber往后移动了一位,就会出现oldFiber.index>newIdx的情况?
答:正如前面,jsx出现空槽,而不是fiber。上图可以看到ul的子虚拟dom li。第一次是current树是空的,循环jsx创建的fiber,fiber.index是循环jsx的下标,循环到第二次jsx是null不创建fiber,循环到第三次创建fiber,fiber.index是2。
正文 reconcileChildrenArray
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
{
var knownKeys = null;
for (var i = 0; i < newChildren.length; i++) {
var child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}
var resultingFirstChild = null;
var previousNewFiber = null;
var oldFiber = currentFirstChild;
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;
// 1 一对一顺序比较新的子节点和旧的子节点
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 1.1
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 1.2 updateSlot会对比key,如果key相同,内部会继续调用updateElement等比较type
// └─如果key 继续比较type
// └─type相同 复用fiber, newFiber不为null
// └─type不同 创建新的fiber节点,newFiber不为空,oldFiber不为空,但是newFiber.alternate为空,
// └─如果key不同 newFiber为null
var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
// 1.2.1 key不同 break退出1对1顺序比较,保持newIdx位置,例如头两个节点key相同,直到第三个节点key不同
break;
}
if (shouldTrackSideEffects) {
// 1.2.2 key相同type不同,把旧fiber标记为删除。例如原来是<li key="b">现在是<p key="b">
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 1.2.3 把newFiber标记为添加,和添加的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 记录头节点,第一个新子节点
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 2. 一比一顺序比较后,如果新的子节点遍历完了,剩下的所有的旧子节点标记为删除
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
var numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
// 返回第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
// 返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
return resultingFirstChild;
}
// 3. 一比一顺序比较后,如果旧子节点没有了,但是新的子节点还有,创建对应的fiber,标记为添加
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (_newFiber === null) {
continue;
}
// 3.1 新的fiber标记为添加
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 第一次循环 标记为第一个节点
resultingFirstChild = _newFiber;
} else {
// 第二次 第三次...作为前一个节点的兄弟节点
previousNewFiber.sibling = _newFiber;
}
// 第一次 第二次 第三次...缓存为前一个节点
previousNewFiber = _newFiber;
}
if (getIsHydrating()) {
var _numberOfForks = newIdx;
pushTreeFork(returnFiber, _numberOfForks);
}
// 返回所有新子节点的第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
// 返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
return resultingFirstChild;
} // Add all children to a key map for quick lookups.
// 4. 一对一顺序比较完了,旧子节点和新子节点都有剩余
// 4.1 把剩下的旧子节点放入map中
var existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
// 4.2 看能否复用旧的fiber, updateFromMap内部一样会判断key,内部继续调用updateElement等继续判断type,像updateSlot
var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
// 4.2.1 key相同
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
// 4.2.2 type也相同,只有key type都相同,updateFromMap内部才会复用旧fiber构建alternate,key相同type不同创建新fiber,不会给alternate赋值
if (_newFiber2.alternate !== null) {
// 4.2.3 完全复用了旧fiber,从剩余的旧子节点中移除它
existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
}
}
// 4.2.4 新fiber标记为添加
lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}
previousNewFiber = _newFiber2;
}
}
if (shouldTrackSideEffects) {
// 4.2.5 旧fiber不能复用,例如key相同type不同,把旧fiber标记为删除
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
if (getIsHydrating()) {
var _numberOfForks2 = newIdx;
pushTreeFork(returnFiber, _numberOfForks2);
}
// 返回第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
// 返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
return resultingFirstChild;
}
写在最后
源码中除了主要的三个for循环,placeChild是同样重要的细节,每一轮for都有一个placeChild,给每个Fiber打上插入(Placement)移动(Placement)删除(Deletion)标记。