魔幻的key
key在vue和react中都有用到,都是用来diff的,那么key到底是用来做什么的?
以下是vue官网中对于key的解释。
Vue 默认按照“就地更新”的策略来更新通过
v-for渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染
所以如果列表渲染中,每个列表项的dom都不大一样或者非常简单的时候,不用key可以减少dom的重建,但是如果列表项中包含子组件,子组件又依赖父组件,不添加key的情况,很容易造成列表变化没有引起diff重新渲染。
结论:key在react/vue中都是为了在diff的过程中进行优化的一种手段。只是使用的方式不同。
react diff 过程
react中的diff又叫reconcile.即协调
对不同的节点有不同的协调过程:
单节点
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 从左到右的遍历
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
// switch case
} else {
deleteChild(returnFiber, child);
}
// 下一个节点
child = child.sibling;
}
// 创建新Fiber,并返回
数组
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// old和new均没有到达尾部的时候
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
// oldFiber比较完了,直接跳出循环
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// 指针向后走
nextOldFiber = oldFiber.sibling;
}
// 更新fiber, updateSlot 在key不相等或不存在的时候会返回null,否则会返回一个fiber
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// 无可复用的fibber(key不相等或者不存在),break循环
if (newFiber === null) {
// oldFiber比较完了
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 如果有sideEffect
if (shouldTrackSideEffects) {
// 旧的子元素 Fiber 链表中是否存在当前位置的 Fiber 对象,
// 同时新的 Fiber 对象的 `alternate` 属性是否为 `null`。
// 如果这两个条件都满足,说明当前位置的 Fiber 对象没有被重用,需要将其从 Fiber 树中删除。
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 记录比较的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 新链表的头指针记录,只有第一个元素会走到这
resultingFirstChild = newFiber;
} else {
// 新链表的头指针,指向下一个位置,新增
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// newArray已经走完了,删除fiber中剩余的元素
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
if (oldFiber === null) {
// 如果oldFiber没走,则需要新增newArray中的数据到previousNewFiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
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;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
// newArray 和OldFiber都没走完,将fiber变成key-fiber的一个map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
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.
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));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
从上面的源码可以看出,在diff的过程中,发生了两次遍历,第一次是将newArray和oldFiber进行比较,在比较的过程中同时会生成一个newFiber:
- oldFiber与newArray比较,如果oldFiber或者Array都已经比遍历结束,则跳出循环。
- oldFiber和newArray的当前值比较,如果是key相同,type相同,则用新的array的props更新新的fiber
- key相同,type不同,删除旧的,新增新的
- key type都不同,跳出循环
第二次比较:
- 只剩下旧的节点(删除旧节点)
- 只剩下新的节点(新增新节点)
- 都有剩下则进行下一步的处理:
- 将旧的fiber用一个key-fiber的map存起来
- 遍历新的数组,如果在旧的fiber中找到fiber,则利用旧的fiber并更新并删除key,否则新建
- 删除map中剩余的key和fiber.(多余的)
比较react和Vue
相同点:
没有key会默认使用index作为key,则发生元素删除活添加的时候,会重新渲染整个列表,而不是只渲染删除或者增加的元素
不同点:
react中的链表是单链表,有一个指针指向兄弟节点,一个指针指向父亲节点,diff的过程只能单向遍历。
vue中的链表是双向链表,所以可以进行首尾遍历,进行优化。