react 学习之从diff children看key的合理使用
大部分优化环节react都自己在内部处理了,但有一种情况也值得开发者注意,那就是列表中key的使用,合理的使用key有助于能精确的找到用于新旧节点复用的老节点。那么我们这里来学习下react 是如何diff children的,从源码的角度看。
用几个案例来描述React diffChild核心流程,react在一次更新中,当发现通过render得到的children如果是一个数组的话,就会调用reconcileChildArray来调和子代fiber,整个对比的流程就是在这个函数中进行的。
diff children流程
第一步:遍历新children,复用oldFiber
function reconcileChildrenArray() { // 第一步 // oldFiber 存在;newIdx应该是一个下标指针 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { // 当前循环的旧fiber节点下标大于当前循环小标时 // nextOldFiber装这个旧fiber if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null } else { // 否则,nextOldFiber装它的兄弟节点,也就是下一个需要循环的节点 nextOldFiber = oldFiber.sibling } // updateSlot会判断当前的tag和key是否匹配,如果匹配复用老fiber形成新的fiber,如果不匹配,返回Null. // returnFiber就是当前oldFiber的父节点,newChildren[newIdx]与oldFiber对应的新的节点,expirationTime过期时间 const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime) if (newFiber === null) break // ..一些其他逻辑 // shouldTrackSideEffects为更新流程 if(shouldTrackSideEffects){ // 找到了与新节点对应的fiber,但是不能复用 // alternate是一个指针,在双缓存树中,workInProgress(内存中构建的树)和current(渲染树)使用alternate指针相互指向 if(oldFiber && newFiber.alternate === null){ deleteChild(returnFiber,oldFiber) } } } }
第二步,统一删除oldfiber
if(newIdx === newChildren.length){ deleteRemainingChildren(returnFiber,oldFiber); return resultingFirstChild }
这一步其实就是,当第一步遍历完 newIdx === newChildren.length,此时证明所有newChild已经全部被遍历完,那么剩下的oldFiber就没有用了,就统一删除
第三步,统一创建newFiber
if(oldFiber === null){ for(;newIdx < newChildren.length;newIdx++){ const newFiber = createChild(returnFiber,newChildren[newIdx],expirationTime) // ... } }
第三步适合如下情况,当经历第一步,oldFiber为null,证明oldFiber复用完毕,那么如果还有新的children,说明都是新的元素,只需要调用createChild创建新的fiber
第四步:针对发生移动和更复杂的情况
// 将所有子元素添加到一个键映射中,以便快速查找。 const existingChildren = mapRemainingChildren(returnFiber, oldFiber) for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap(existingChildren, returnFiber) // 从mapRemainingChildren删掉已经复用oldFiber }
1.mapRemainingChildren返回一个map,map里存放剩余的老的fiber和对应的key(或Index)的映射关系
2.接下来遍历剩下没有处理的Children,通过updateFromMap判断existingChildren中有没有可以复用oldFiber,如果有,那么复用,如果没有,新创建一个newFiber
第五步:删除剩余没有复用的oldFIber
if (shouldTrackSideEffects) { // 检查是否存在未被处理的子节点,如果存在,则将它们添加到删除列表中 existingChildren.forEach(child => deleteChild(returnFiber, child)); }
这里有个疑问,不是第二步已经删除了oldFiber了吗,怎么这里还要再删除一遍呢。这里得注意,第二步执行的前提条件是当前新节点已经被遍历完了之后才删除的。比如
oldChild: A B C D
newChild: A B
这种情况就只会到第二步删除就完毕了,
关于为什么不能使用Index作为Key
这里我们先来考虑使用math.random作为key会怎么样
如果math.random()作为key,那么每次更新,key都会变,等于key的复用没有用。从而使所有的组件和DOM元素每次都要重新创建。这不仅会造成允许变慢的问题,更有可能导致用户输入的丢失。
同理,使用Index作为key也是一样的,如果选择index作为key,元素发生移动,那么从移动节点开始,后面的fiber都不能做到合理的复用了。