Repository navigation
Key
key 的好处无非是可以复用 oldFiber,只需通过调整位置来实现新的界面。理论简单,但是实操,并不是一眼就明了都。
回顾我们之前的 reconciler ,过程比较简单。
整个流程下来大概做了这几件事
- 遍历 childElements
- 文本节点 & Fragment 的 ele 进行特殊处理,其实就是 tag 打上对应的标签。
- 同类型节点=>Update=>更新 props=>之后直接更新 dom 元素的 attr。
- 不同类型则直接替换,原 fiber 打上 DEL 的 effectTag。
- 如果 oldChildElement 比 childElement 的长度还长,则后续的 oldChildElement 直接标记 deletion 。
首先 1&2 是仍然是要做的,后续有所不同,由于引入了 key ,除了考虑 type 是否一致,还需要考虑 key 是否一致,如何处理判断逻辑比较好呢。 自然是看看源码是如何实现的。

纵观全局,不难看出,函数逻辑大致可分为两块。
既,根据 oldFiber 是否为 null 可分为 【插入】 和 【变动】操作两大块。
当 oldFiber 为null (最初的 oldFiber 来自于 returnFiber.alternate.child),表示没有可复用的 oldFiber,可视为newChild是 均为新插入元素,此时只需经过 l23-l41 就是一个完整的支线。
- oldFiber === null ; 此时先完成我们上述的 1&2 两步,然后创建 effectTag 为 placement 的 newFiber,然后是代码中常见的变更 preFiber,定义 sibling 操作,直至循环结束,最后连接一下 resultingFirstChild 和 wipFiber 即可,这条支线比较简单。
- oldFiber !== null ; l46-l51 的判断其实不是特别明确其用意(望知道的大佬指点迷津),然后是创建 newFiber ,注意这里调用的是 updateSlot 而不是分支1的 createChild,updateSlot 只会在 oldFiber 和 ele 的 key 值相等时返回有效的 fiber ,否则返回 null。这里我们先假设所有子元素不带 key,那和我们旧的逻辑差不多, type 相同则返回 effectTag 为 update 的 fiber,不相同,则返回 placement fiber,l66-l68 还对 type 不同的 oldFiber 进行 del 标记,也就是 effectTag 设置为 Deletion,同时将其放入 wip,也就是新构建的 父节点的 effects 队列里。后续操作和分支1一样, 注意由于可能存在 oldChilds 比 newChilds 长度更长的情况,此时还需要对多余的 oldFiber 进行 del 标记(l80-l84),最后返回。**回到刚才的逻辑,假设存在子元素带key且与对应 oldFiber 的key不相等。**那么 newFiber 就是 null ,然后当前的 for 循环 break,l55-l61。此时直达l86,先用一个 Map 储存 oldFiber 及其 sibling,键值为key或者index。然后接着刚才的循环,此时 newFiber 来自 updateFromMap,这里的逻辑不外乎是得出 newFiber 的 effectTag,updateFromMap 的逻辑也比较简单——如果能从 map 中拿到对应的 oldFiber,那就走 updateSlot 否则 走 createChild。那 newFiber 的位置如何定义的?别着急,我们接着往下看,l100 => placeChild。
placeChild 决定了 newFiber 是移动还是不移动,l17-l20 就是简单的插入操作,这里需要仔细思考的是l9-l16。如果 旧位置 在 新位置之前,则打上 placement 的 tag,否则不处理(保持原位)。
最后记得删除没从 map 里 get 过的 fiber,也就是还在 map 里的fiber,然后 连接 firstChild 和 returnFiber 。
reconcileChildrenArray 完。
然而加入 key 要做的处理还没有结束——在 commitWork 阶段对 Placement 也需要特殊的处理了。
function commitPlacement(parentDom: HTMLElement, fiber: Fiber): void {
const before: HTMLElement | null = getHostSibling(fiber);
if (before) {
/* insertBefore 则调整位置到指定元素前一个 */
parentDom.insertBefore(fiber.stateNode, before)
} else {
/* appendChild 同一个元素相当于调整其位置到最后 */
parentDom.appendChild(fiber.stateNode);
}
}任何 effectTag 为 Placement 的 fiber 都将转发到这个方法内,parentDom,是 fiber 父节点里第一个为 Host 的 dom 对象,before 来自于fiber 的 sibling 中 最近的、为 Host 元素 且 稳定 的 fiber 节点的 dom 对象。
这里的稳定 react 是这么判断的,此时的 node 是当前某个 sibling 节点,可知只有 sibling 节点为 update 时,才是稳定的,毕竟 newFiber 不存在tag 为 Deletion 的。如果不存在这样的 sibling 节点则返回 null,那么 before 就是 null 值了。
/* FIBER effectTag */
export const Placement = /* */ 0b000000000010; //2
export const Update = /* */ 0b000000000100; //4
export const PlacementAndUpdate = /* */ 0b000000000110; //5
export const Deletion = /* */ 0b000000001000; //8
// ...
if (!(node.effectTag & Placement)) {
return node.stateNode;
}回到 commitPlacement 中,对于数组顺序均被打乱的情况,此时所有的节点会被父节点调用 appendChild,相当于放在此时所有子节点的最后,而调用顺序又是按更新后的排序顺序,相当于在队列A中,按某个顺序,依次提出队员,并放在队伍最后,执行完所有操作后,队列就会按提出的顺序排列了。但是因为 placeChild 的存在,这里并不是一个 O(n) 的问题,react 能做到复用一小块的顺序,例如 0123 => 2310 ,react 甚至不会对 2 和 3 的节点打上任何 effectTag 标签,只需将 1 和 0 依次安排到最后即可。
以上几乎是添加 key 功能新增的逻辑部分,想看更多源码可以看 reconciler.ts。