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都不能做到合理的复用了。

 

posted @ 2024-09-07 12:10  飞向火星  阅读(3)  评论(0编辑  收藏  举报