无论是在 vue 还是 react 中,使用 key 都是一个 必须面对的问题,最近就对 react 的key 做了一点学习,这里就当做个人的学习笔记了
我们就来看这里的 (已删除 多余的代码,只留下了 数组类型的 子节点 )
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null, // 子节点的Fiber
newChild: any, // 子节点的ReactElement
expirationTime: ExpirationTime, // fiber 循环中的 过期时间
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
// 这里 就是 处理 <></> 这样的子节点的,因为 这样的 节点是 没有 key 的,就把内部的
// 子节点 统一处理 为 数组子节点
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
...
// 这里就是 当子节点 是数组的情况
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}
...
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
接下来再 看 看看 reconcileChildrenArray
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
// 这里的 节点 是一个 链表的形式,只不过 next 变成了 sibling
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 以相同的顺序 去遍历 新老节点,然后判断他的key 是否相同
// 如果遇到 第一个 不相同的,则 立刻 跳出循环
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
// 当没有复用节点的情况下(也就是 key 不相同) 直接跳出循环
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// shouldTrackSideEffects 这个参数 先放一边,是主函数 传进来使用的
if (shouldTrackSideEffects) {
// 没有复用 之前的节点的话,直接 删除 老节点的 所有子节点
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果相等,则表示 全部遍历 完成
if (newIdx === newChildren.length) {
// 删除多余的节点
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
// 返回第一个子节点
// 因为这里是一个 链表,只要知道 head 就可以知道所有的节点了
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
// 老节点 遍历完成,新的节点 还有节点 没有遍历到,那么创建 节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (!newFiber) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// 指向自己的兄弟节点如前文所说,建立 链表关系
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
// 创建一个 es6 中的 Map,并以 key 作为键,这个函数 后续会讲到
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
// 先 看 是否是 number string 节点,是的话,直接去找 index(没有 key
// 然后根据 key 或者 index 去获取节点 (这里的 existingChildren 如前文所讲,就是一个 map)
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
if (newFiber) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
// 如果复用了这个节点,那么 就 把 这个节点 从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) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
看一下 上面函数中用到的 第一个函数 updateSlot
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
const key = oldFiber !== null ? oldFiber.key : null;
// 如果是 文本节点,直接复用
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
if (key !== null) {
return null;
}
return updateTextNode(
returnFiber,
oldFiber,
'' + newChild,
expirationTime,
);
}
// 如果不是文本节点的话
if (typeof newChild === 'object' && newChild !== null) {
// 根据 各个 $$type 进行判断(这里 每种 不同的 react 节点 都有 不同的 $$type ,默认是 symbol 类型)
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 只有当 key 相等的情况下,才会复用 这个节点
// 可以看到,之前 中如果没有 key 的话,就是 null,而 null === null
// 所以 如果没有写 key ,就会 默认复用 之前的节点
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
expirationTime,
key,
);
}
return updateElement(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
} else {
// 否则返回 null 表示 不能复用当前节点,key 不相等,就放弃
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
} else {
return null;
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(
returnFiber,
oldFiber,
newChild,
expirationTime,
null,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType();
}
}
return null;
}
再来看一下 mapRemainingChildren 函数
// 这个函数其实很简单
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
// instead.
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
// 创建一个 map 对象,并以 key 或者 index 作为 键
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
到这里了,就可以粗略的总结一下 key 发挥的作用了
- 先是 遍历一遍 新老节点,如果相同的话,直接复用,如果 都没有写key,默认 key 是一样的(都是 null),接着判断 $$type 是否相等
- 遇到第一个不相同的,直接 break循环
- 比较 当前 新老节点 遍历的长度,分别做处理
- 然后 根据 老节点 创建 一个 key | index 作为 key 的 map,所以说如果你使用 index 作为 key 的话,其实没有任何 用处,默认就是这样的
- 然后 新节点 使用 key 在 创建的 map 中取,如果有对应的可复用的节点,就复用