【React源码学习】4 Diff算法 多节点Diff


本次实验demo

export default function App() {
    const [toggle, setToggle] = useState(true);
    const a = (
        <div>
            <p key="0" name='0'>0</p>
            <p key="1">1</p>
        </div>
    );
    const b = (
        <div>
            <p key="0" name='00'>0</p>
            <p key="1">1</p>
        </div>
    );

    return (
        <div
            onClick={() => {
                setToggle(!toggle);
            }}
        >
            {toggle ? a : b}
        </div>
    );
}

updateHostComponent(beginWork内)

var nextProps = workInProgress.pendingProps;
var prevProps = current !== null ? current.memoizedProps : null;
var nextChildren = nextProps.children;

workInProgress时新的props,如果jsx对象出现变化,则workInProgress发生变化。

current.memoizedProps代表当前被渲染的fiber的props,diff对比的就是这两个props

nextProps.children是jxs对象的child,props用来区分单节点diff还是多节点diff

reconcileChildren

if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}

current === null说明同层的fiber不存在,react直接进入mount逻辑(创建新的fiber,跳过diff),反之调用reconcileChildFibers开始diff。

mountChildFibers和reconcileChildFibers都是childReconciler的返回值

reconcileChildFibers

reconcileChildFibers根据element.$$typeof决定diff的方式,多节点diff时$$typeof === array。调用reconcileChildrenArray开始多节点diff

if (isArray$1(newChild)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}

reconcileChildFibers的作用就是调用不同的diff算法

fiber复用时reconcileChildrenArray

看一下此时reconcileChildrenArray的参数

  1. returnFiber -> workInProgress fiber
  2. currentFirstChild -> current.child (fiber)
  3. newChild -> workInProgress的子jsx

diff之前先看一下这几个比较重要的变量

var resultingFirstChild = null;
//diff结束后返回的fiber,是workInProgress的子节点

var previousNewFiber = null;
//上一次对比的fiber

var oldFiber = currentFirstChild;
//正在被对比的current child fiber

var lastPlacedIndex = 0;
//最新插入的fiber的index

var newIdx = 0;
var nextOldFiber = null;
//下一次要被对比的current child fiber(old fiber.sibling)

多节点diff涉及到三个for循环, 看一下第一个for循环。

第一个for循环按照数组的顺序从左向右依次对比,调用updateSlot,updateSlot根据jsx.$$typeof决定对比方式

case REACT_ELEMENT_TYPE:
{
    if (newChild.key === key) {
        if (newChild.type === REACT_FRAGMENT_TYPE) {
            return updateFragment(returnFiber, oldFiber, newChild.props.children, lanes, key);
        }
        return updateElement(returnFiber, oldFiber, newChild, lanes);
    } else {
        return null;
    }
}

对比key,如果key相同,则调用updateElement,看一下updateElement内部的逻辑

if (current.elementType === element.type || (isCompatibleFamilyForHotReloading(current, element) )) {
    var existing = useFiber(current, element.props);
    existing.ref = coerceRef(returnFiber, current, element);
    existing.return = returnFiber;
    return existing;
}

key相等的前提下对比type,如果type叶相等调用useFiber对current.child进行复用

function useFiber(fiber, pendingProps) {
    var clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
}

useFiber返回可复用的fiber

lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

type改变时的reconcileChildrenArray

demo(替换之前的a, b)

const a = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
    </div>
    );
    const b = (
        <div>
            <div key="0">0</div>
            <p key="1">1</p>
        </div>
    );

updateElement内根据workInProgress创建一个新的fiber,并设置好父子关系

var created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;

reconcileChildrenArray的第一个for循环内,如果newFiber.alternate === null(fiber是新建的,alternate并不会指向其他fiber,如果是复用的fiber,alternate应该指向current fiber)。deleteChild给oldFiber打上Deletion flag,便于后面删除

if (oldFiber && newFiber.alternate === null) {
    deleteChild(returnFiber, oldFiber);
}

列表内新增DOM情况下reconcileChildren

demo(替换之前的a, b)

const a = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
    </div>
);
const b = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
        <p key="2">2</p>
    </div>
);

我们在最后面添加了一个元素,reconcileChildrenArray循环到最后一个对象时进入下面的代码(最后一个fiber没有对象的alternate,因此oldfiber === null)

if (oldFiber === null) {
  for (; newIdx < newChildren.length; newIdx++) {
    var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);

    if (_newFiber === null) {
      continue;
    }
    lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = _newFiber;
    } else {
      previousNewFiber.sibling = _newFiber;
    }
    previousNewFiber = _newFiber;
  }
  return resultingFirstChild;
}

新建fiber后打上Placement flag,然后将previousNewFiber.sibling指向newFiber。

删除列表元素时reconcileChildrenArray

demo

const a = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
        <p key="2">2</p>
    </div>
);
const b = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
    </div>
);

两次循环后newChildren遍历结束,而oldFiber还有剩余元素,进入下面的逻辑

if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
}

deleteRemainingChildren会给oldFiber及其sibling fiber全都打上Deletion Flag,用于后面删除

fiber移动时reconcileChildrenArray

demo

const a = (
    <div>
        <p key="0">0</p>
        <p key="1">1</p>
    </div>
);
const b = (
    <div>
        <p key="1">1</p>
        <p key="0">0</p>
    </div>
);

在第一个for循环中由于两个p节点不同key,因此break掉第一个循环

var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
    if (oldFiber === null) {
        oldFiber = nextOldFiber;
    }
    break;
}

这个地方break掉说明存在fiber移动的可能

tip:因此列表添加key很重要,没有key则直接新建和删除fiber,造成大量性能浪费,如果有唯一key,React会尝试复用fiber

因key不相同跳出第一个for循环后,React将剩余的oldFiber保存在一个map中,便于后面查找index

var existingChildren = mapRemainingChildren(returnFiber, oldFiber)

for循环中根据当前newFiber的key在map中找到对应的fiber,找到后从map中删除fiber

var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);

在placeChild内

if (oldIndex < lastPlacedIndex) {
    newFiber.flags = Placement;
    return lastPlacedIndex;
} else {
    return oldIndex;
}

如果oldIndex > lastPlacedIndex说明该元素前面的元素变化,该元素的相对位置不变,将oldIndex赋值给lastPlacedIndex即可,用于后面元素比较。

oldIndex < lastPlacedIndex 说明相对于原来的位置,该元素被放在了更后面,要打上Placement flag(这过程有点快排的感觉)

summary

先总结以下三次for循环的作用

第一个for

  1. 对比key,若key type相同则复用、
  2. key相同 type不同会创建新的fiber并打上Placement flag,oldFiber打上Deletion flag。
  3. key不同直接break

第一个for结束后如果jsx遍历结束(newIdx === newChildren.length),则删除oldFiber及其所有的sibling

第二个for

第二个for的进入条件是oldFiber === null,说明oldFiber遍历结束,此时jsx对象可能还有甚于,需要处理插入逻辑

  1. 给剩下的jsx对象创建fiber并打上placement

第三个for

处理fiber移动的问题,将oldFiber及其sibling保存在map中(key -> fiber),接下来对比其oldIndex和newIndex,如果oldIndex > newIndex说明前面的元素小时,旧的元素顺序不变,但是lastPlacedIndex=oldIndex,便于确定后面fiber的顺序。如果oldIndex < newIndex, 说明向后移动,要打上Placement flag。

多节点diff总结

多节点diff调用过程


Author: Maple
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Maple !
  TOC