Skip to content
On this page

针对有 多个同级节点 的情况

多节点变化:

  1. 节点更新
    1. 节点属性更新(key,props)
    2. 节点类型(div -> p)
  2. 节点数量新增或者减少
  3. 节点位置顺序变化

Diff 算法

经历两轮遍历: 第一轮:处理需要更新的节点 第二轮:处理其他类型节点

reconcileChildrenArray(第一轮循环)

js
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
	// 初始化变量
	let resultingFirstChild = null // 最终会返回给 wip 的childFiber
	let previousNewFiber = null // 更新过程中记录上个Fiber(用于sibling连接)
	let oldFiber = currentFirstChild; // 旧列表当前节点
	let lastPlacedIndex = 0; // 旧节点列表循环到的index,用于插入新节点定位
	let newIdx = 0; // 新节点列表的index
	let nextOldFiber = null;
	
	// 第一轮循环,处理节点更新
	for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
		if (oldFiber.index > newIdx) {
			nextOldFiber = oldFiber;
			oldFiber = null;
		} else {
			nextOldFiber = oldFiber.sibling;
		}
		// 判断 DOM 节点能否复用
		const newFiber = updateSlot(
			returnFiber,
			oldFiber,
			newChildren[newIdx],
			lanes,
		);
		if (newFiber === null) {
			if (oldFiber === null) {
				oldFiber = nextOldFiber;
			}
			// DOM 不能复用,跳出第一轮循环
			// 从 updateSlot 来看: key 不相同则会跳出第一轮循环
			// key不相同,属于位置变化,不属于节点更新
			break;
		}
		if (shouldTrackSideEffects) {
			// type 不相同,标记删除旧节点
			if (oldFiber && newFiber.alternate === null) {
				deleteChild(returnFiber, oldFiber);
			}
		}
		// 标记插入新节点
		lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
		if (previousNewFiber === null) {
			resultingFirstChild = newFiber; // 更新过程创建的第一个Fiber
		} else {
			previousNewFiber.sibling = newFiber; // 通过sibling连接下一个节点
		}
		previousNewFiber = newFiber; // 更新成新创建的节点
		oldFiber = nextOldFiber;
	}
}

updateSlot

js
function updateSlot(returnFiber, oldFiber, newChild, lane) {
	// 比较旧节点和新节点的key,不相同则返回null,相同就更新节点
	const key = oldFiber !== null ? oldFiber.key : null;
	//...
	if (typeof newChild === 'object' && newChild !== null) {
		switch (newChild.$$typeof) {
				case REACT_ELEMENT_TYPE: {
					if (newChild.key === key) {
						// 相同key 返回更新后的节点
						return updateElement(returnFiber, oldFiber, newChild, lanes)
					}
					else {
						return null
					}
				}
			}
		}
	}
}

function updateElement(returnFiber, current, element, lanes) {
	if (current !== null) {
		if (current.elementType === element.type) {
			// 前后 组件type 没有变(div -> div)
			const existing = useFiber(current, element.props);
			existing.ref = coerceRef(returnFiber, current, element);
			existing.return = returnFiber;
			return existing;
		}
		// ...
	}
	// 前后 Fiber 的type 不一样,创建新的 Fiber返回
	const created = createFiberFromElement(element, returnFiber.mode, lanes);
	created.ref = coerceRef(returnFiber, current, element);
	created.return = returnFiber;
	return created;
}

placeChild

js
function placeChild(newFiber,lastPlacedIndex,newIndex) {
	newFiber.index = newIndex; // 赋值当前新节点index
	const current = newFiber.alternate; // 新节点对应复用的节点
	 if (current !== null) {
		 // 有可复用节点
		const oldIndex = current.index; // 当前旧节点index
		if (oldIndex < lastPlacedIndex) {
			// This is a move.
			// 当前需要插入的节点位置 大于 原来的位置,也就是要将节点后移
			newFiber.flags = Placement;
			return lastPlacedIndex;
		} else {
			// This item can stay in place.
			// 节点位置没有变化或者节点位置大于现在需要插入的位置
			return oldIndex;
		}
	} else {
		// This is an insertion.
		// 没有复用节点,直接插入
		// 插入的过程标记
		newFiber.flags = Placement;
		return lastPlacedIndex;
	}
}

小结

跳出 第一轮遍历 的条件:

  1. key 不相同,此时可能 新节点列表 还没遍历结束
  2. 旧列表 或者 新节点 遍历结束

遍历结束跳出循环

js
function reconcileChildrenArray() {
	// ...	
	if (newIdx === newChildren.length) {
		// 新节点列表遍历结束,删除剩余旧节点
		deleteRemainingChildren(returnFiber, oldFiber);
		// 返回创建的节点列表
		return resultingFirstChild;
	}

	// 新节点没遍历完,旧节点先遍历完
	if (oldFiber === null) {
		for (; newIdx < newChildren.length; newIdx++) {
			// 添加剩余的 新节点
			const 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;
	}
}

第二轮循环

新旧节点列表都没遍历完,但节点位置有不同

js
function reconcileChildrenArray() {
	// ...接上面代码
	// 将剩下的 旧节点,做成 map (key或者index作为map的key,value是节点)
	const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
	for (; newIdx < newChildren.length; newIdx++) {
		// 从map中找到对应的节点 或者 创建新的节点
		const newFiber = updateFromMap(
			existingChildren,
			returnFiber,
			newIdx,
			newChildren[newIdx],
			lanes,
		);
		if (newFiber !== null) {
			if (shouldTrackSideEffects) {
				if (newFiber.alternate !== null) {
					// 如果 map 有对应的节点,就删除map中元素
					existingChildren.delete(
						newFiber.key === null ? newIdx : newFiber.key,
					);
				}
			}
			// 标记插入
			lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
			if (previousNewFiber === null) {
				resultingFirstChild = newFiber;
			} else {
				previousNewFiber.sibling = newFiber;
			}
			previousNewFiber = newFiber;
		}
	}
	if (shouldTrackSideEffects) {
		// 删除多余节点
		existingChildren.forEach(child => deleteChild(returnFiber, child));
	}

	return resultingFirstChild;
}