本次实验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的参数
- returnFiber -> workInProgress fiber
- currentFirstChild -> current.child (fiber)
- 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
- 对比key,若key type相同则复用、
- key相同 type不同会创建新的fiber并打上Placement flag,oldFiber打上Deletion flag。
- key不同直接break
第一个for结束后如果jsx遍历结束(newIdx === newChildren.length),则删除oldFiber及其所有的sibling
第二个for
第二个for的进入条件是oldFiber === null,说明oldFiber遍历结束,此时jsx对象可能还有甚于,需要处理插入逻辑
- 给剩下的jsx对象创建fiber并打上placement
第三个for
处理fiber移动的问题,将oldFiber及其sibling保存在map中(key -> fiber),接下来对比其oldIndex和newIndex,如果oldIndex > newIndex说明前面的元素小时,旧的元素顺序不变,但是lastPlacedIndex=oldIndex,便于确定后面fiber的顺序。如果oldIndex < newIndex, 说明向后移动,要打上Placement flag。