react中DOM DIFF的三个规则
- 只对同级元素进行比较,不同层级不比较
- 不同的类型对应不同的元素
- 可以通过key来标识同一个节点
react中DOM DIFF的遍历规则
-
第一轮
- 如果key不同,则直接结束本轮循环
- newChildren和oldFiber遍历完,结束本轮循环
- key相同而type不同,标记老的oldFiber为删除,继续循环
- key相同type相同,则可以复用老节点(oldFiber),继续循环
-
第二轮
- newChildren遍历完而oldFiber还有,遍历剩下的所有oldFiber标记为删除,DIFF结束
- oldFiber遍历完了,而newChildren还有,将剩下的newChildren标记为插入,DIFF结束
- newChildren和oldFiber都同时遍历完,DIFF结束
- newChildren和oldFiber都没有完成,则进行节点移动的逻辑
-
第三轮
- 处理节点移动的情况
多个节点的数量和key相同,有的type不同
如上图所示,多个节点的数量和key相同,有的type不同。
/**
* 如果新的虚拟DOM是一个数组的话, 也就是说有多个儿子的话
* @param {*} returnFiber ui
* @param {*} currentFirstChild null
* @param {*} newChild [liA,liB,liC]
*/
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 如果没有老fiber,也就是初次挂载的时候
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
这个是之前reconcileChildrenArray处理没有oldFiber的情况,现在做其他情况的处理
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// +++++++++++++++++++++++++++++++++++++++++++++++++
//下一个老fiber
let nextOldFiber = null;
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用老fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; // 跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++
// 如果没有老fiber,也就是初次挂载的时候
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
updateSlot试图复用老fiber
function updateSlot(returnFiber, oldFiber, newChild) {
const key = oldFiber ? oldFiber.key : null;
//如果新的虚拟DOM的key和老fiber的key一样
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild);
} else {
//如果key不一样,直接结束返回null
return null;
}
}
更新元素
function updateElement(returnFiber, oldFiber, newChild) {
if (oldFiber) {
if (oldFiber.type === newChild.type) {
const existing = useFiber(oldFiber, newChild.props);
existing.return = returnFiber;
return existing;
}
}
//如果没有老fiber
const created = createFiberFromElement(newChild);
created.return = returnFiber;
return created;
}
function placeChild(newFiber, lastPlacedIndex, newIdx) {
newFiber.index = newIdx;
if (!shouldTrackSideEffects) {
return;
}
const current = newFiber.alternate;
//如果有current说是更新,复用老节点的更新,不会添加Placement
if (current) {
// TODO
} else {
newFiber.flags = Placement;
}
}
目前完整的reconcileChildrenArray代码逻辑如下,加了老fiber和新fiber都存在的处理逻辑,按照li(A),li(B),li©到li(A),p(B),li©的更新逻辑,li(A)通过updateSlot的处理,是可以复用的,updateElement就是处理更新或者创建fiber;li(B)的时候updateSlot返回了新创建的p(B),并且需要删除li(B),oldFiber && !newFiber.alternate这种情况就符合了li(B)的情况,placeChild就是把p(B)标记为插入;li©的情况和li(A)一样。
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber,newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果没有老fiber,也就是初次挂载的时候
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
来简单测试下:
<div>
<button id="multi1">5.多个节点的数量和key相同,有的type不同</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B">B</li><br/>
<li key="C"id="C">C</li><br/>
</ul><br/>
<button id="multi1Update">更新属性,type不同的删除老节点,删除新节点</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<p key="B" id="B2">B2</p><br/>
<li key="C" id="C2" >C2</li><br/>
</ul><br/>
</div>
<hr/>
multi1.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B">B</li>
<li key="C" id="C">C</li>
</ul>
);
ReactDOM.render(element, root);
});
multi1Update.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<p key="B" id="B2">B2</p>
<li key="C" id="C2">C2</li>
</ul>
);
ReactDOM.render(element, root);
});
可以看到搜集的effectList是正确的,但实际上DOM插入的顺序不太对,是因为在之前提交的时候,也就是commitWork阶段commitPlacement插入节点的时候有问题,看原来代码如下:
export function commitPlacement(nextEffect) {
let stateNode = nextEffect.stateNode;
let parentStateNode = getParentStateNode(nextEffect);
appendChild(parentStateNode, stateNode);
}
问题就是出在appendChild的这个时候,做下调整:
export function commitPlacement(nextEffect) {
let stateNode = nextEffect.stateNode;
let parentStateNode = getParentStateNode(nextEffect);
appendChild(parentStateNode, stateNode);
let before = getHostSibling(nextEffect);
if (before) {
insertBefore(parentStateNode, stateNode, before);
} else {
appendChild(parentStateNode, stateNode);
}
}
//当前fiber后面一个离它最近的真实的DOM节点
function getHostSibling(fiber) {
let node = fiber.sibling;
while (node) {
//找它的弟弟们,找到最近一个并且不是插入的节点,返回。。没有更新,更新
if (!( node.flags & Placement )) {
return node.stateNode;
}
node = node.sibling;
}
return null;
}
export function insertBefore(parentInstance, child, before) {
parentInstance.insertBefore(child, before);
}
再来测试下:
可以看到effectList和DOM都是正确的。
多个节点的类型和key全部相同,有新增元素
<div>
<button id="multi2">6.多个节点的类型和key全部相同,有新增元素</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B">B</li><br/>
<li key="C">C</li><br/>
</ul><br/>
<button id="multi2Update">增加新元素并更新老元素</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B2">B2</li><br/>
<li key="C">C</li><br/>
<li key="D">D</li><br/>
</ul><br/>
</div>
<hr/>
multi2.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B">B</li>
<li key="C">C</li>
</ul>
);
ReactDOM.render(element, root);
});
//增加新元素并更新老元素
multi2Update.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B2">B2</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
);
ReactDOM.render(element, root);
});
对于这种情况,就说明oldFiber没有了,新的fiber还有,就会走到if(!oldFiber)的分支去,在里面放置下新fiber就可以了,具体代码就是加一行placeChild(newFiber, newIdx)。
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
debugger
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber,newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
placeChild(newFiber, newIdx)
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
可以看到是更新了B,插入新的D。
多个节点的类型和key全部相同,有删除老元素
<div>
<button id="multi3">7.多个节点的类型和key全部相同,有删除老元素</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B">B</li><br/>
<li key="C">C</li><br/>
</ul><br/>
<button id="multi3Update">删除老元素并更新老元素</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B2">B2</li><br/>
</ul><br/>
</div>
<hr/>
multi3.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B">B</li>
<li key="C">C</li>
</ul>
);
ReactDOM.render(element, root);
});
multi3Update.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B2">B2</li>
</ul>
);
ReactDOM.render(element, root);
});
这个就是从ABC都AB的操作,操作如下:
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
newIdx和newChildren.length相等,就说明新的已经完成了,那就删除所有的之后的老fiber。完整代码如下:
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
placeChild(newFiber, newIdx)
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
可以看到删除了C,并且更新了B。接下来看最复杂的一个。
多个节点数量不同、key不同
<div>
<button id="multi5">9.多个节点数量不同、key不同</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="b">B</li><br/>
<li key="C">C</li><br/>
<li key="D">D</li><br/>
<li key="E">E</li><br/>
<li key="F">F</li><br/>
</ul><br/>
<button id="multi5Update">处理节点移动的情况</button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="C">C</li><br/>
<li key="E">E</li><br/>
<li key="B" id="b2">B2</li><br/>
<li key="G">G</li><br/>
<li key="D">D</li><br/>
</ul><br/>
</div>
<hr/>
multi5.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="b">B</li>
<li key="C">C</li>
<li key="D">D</li>
<li key="E">E</li>
<li key="F">F</li>
</ul>
);
ReactDOM.render(element, root);
});
multi5Update.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="C">C</li>
<li key="E">E</li>
<li key="B" id="b2">B2</li>
<li key="G">G</li>
<li key="D">D</li>
</ul>
);
ReactDOM.render(element, root);
});
这种是最复杂的情况,也是DOM DIFF的精华所在,先来梳理下上图的更新逻辑。
先来看下之前的reconcileChildrenArray的逻辑,按照上图的顺序梳理下。
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
placeChild(newFiber, newIdx)
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
A和A做比较,可以复用;然后比较B和C,key不一样,updateSlot之后返回的null,跳出第一轮循环,newIdx为1,newChildren.length为6,而且oldFiber指向的是B,所以,下面2个判断是进不去的。
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
placeChild(newFiber, newIdx)
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
这个时候就按照上面的流程,把剩下的oldFiber放到existingChildren这个map中去,对应的代码如下:
// 将剩下的老fiber放入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
function mapRemainingChildren(returnFiber, currentFirstChild) {
const existingChildren = new Map();
let existingChild = currentFirstChild;
while (existingChild) {
let key = existingChild.key || existingChild.index;
existingChildren.set(key, existingChild)
existingChild = existingChild.sibling;
}
return existingChildren;
}
这个map目前大概张这样map={B:bFiber,C:cFiber,D:dFiber,E:eFiber,F:fFiber},然后声明一个变量lastPlacedIndex变量,表示不需要移动的老节点的索引,默认为0,接着循环剩下的虚拟dom节点,从C开始。如果能在map中能找到相同key相同type的节点则可以复用老fiber,并把老fiber从map中删除。具体代码如下
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
+ // 指的上一个可以复用的,不需要移动的节点的老索引
+ let lastPlacedIndex = 0;
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
placeChild(newFiber, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
placeChild(newFiber, newIdx)
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
+ //将剩下的老fiber放入map中
+ const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
+ for (; newIdx < newChildren.length; newIdx++) {
+ //去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
+ const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
+ }
}
生成map
function mapRemainingChildren(returnFiber, currentFirstChild) {
const existingChildren = new Map();
let existingChild = currentFirstChild;
while (existingChild) {
let key = existingChild.key || existingChild.index;
existingChildren.set(key, existingChild)
existingChild = existingChild.sibling;
}
return existingChildren;
}
从map中去找到可以复用的fiber
function updateFromMap(existingChildren, returnFiber, newIdx, newChild) {
const matchedFiber = existingChildren.get(newChild.key || newIdx);
return updateElement(returnFiber, matchedFiber, newChild);
}
接着就看下是否能在map中找到可以复用的,把老fiber从map中删除,找不到的话则创建新的fiber节点。
for (; newIdx < newChildren.length; newIdx++) {
//去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
if (newFiber) {
//说明是复用的老fiber
if (newFiber.alternate) {
existingChildren.delete(newFiber.key || newIdx);
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);这个是比较关键的,newFiber.alternate有值,说明是可以复用的老fiber,需要放置该fiber节点,改造下placeChild方法,之前的代码是这样的:
function placeChild(newFiber, newIdx) {
newFiber.index = newIdx;
if (!shouldTrackSideEffects) {
return;
}
const current = newFiber.alternate;
//如果有current说是更新,复用老节点的更新,不会添加Placement
if (current) {
// TODO
} else {
newFiber.flags = Placement;
}
}
改造后的逻辑如下:
function placeChild(newFiber, lastPlacedIndex, newIdx) {
newFiber.index = newIdx;
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
const current = newFiber.alternate;
//如果有current说是更新,复用老节点的更新,不会添加Placement
if (current) {
const oldIndex = current.index;
//如果老fiber它对应的真实DOM挂载的索引比lastPlacedIndex小
if (oldIndex < lastPlacedIndex) {
//老fiber对应的真实DOM就需要移动了
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
//否则 不需要移动 并且把老fiber它的原来的挂载索引返回成为新的lastPlacedIndex
return oldIndex;
}
} else {
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
之前placeChild用到的地方也做出相应的修改;在复用老的fiber的时候,要老fiber节点的索引与lastPlacedIndex做比较:
- 如果小于lastPlacedIndex则需要移动老的fiber, lastPlacedIndex不变
- 如果大于lastPlacedIndex则不需要移动老的fiber,更新lastPlacedIndex为老fiber的index
C在map中能找到,C的索引是2,lastPlacedIndex默认是0,则C是不用移动的,更新lastPlacedIndex为2;同理E也是能在map中找到,也不用移动,E的老索引是4,则更新lastPlacedIndex为4;到B的时候,B的老fiber索引为1,此时lastPlacedIndex为4,那就标记B为移动,lastPlacedIndex不变;到G的时候,在map中找不到可以复用的节点,那就标记为插入;到D的时候,D的老fiber索引为3,lastPlacedIndex为4,标记D为移动,lastPlacedIndex不变,仍然为4。
到这里,新fiber遍历结束了,但是老fiber中的F是没用到的,那就需要把F给删除。那就是在虚拟DOM循环结束后把Map中的所有的剩下的fiber全部标记为删除。
//map中剩下是没有被 复用的,全部删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
完整的代码如下:
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 下一个老fiber
let nextOldFiber = null
// 指的上一个可以复用的,不需要移动的节点的老索引
let lastPlacedIndex = 0;
//处理更新的情况 老fiber和新fiber都存在
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
//先缓存下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图复用才fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
//如果key 不一样,直接跳出第一轮循环
if (!newFiber)
break; //跳出第一轮循环
//老fiber存在,但是新的fiber并没有复用老fiber
if (oldFiber && !newFiber.alternate) {
deleteChild(returnFiber, oldFiber);
}
//核心是给当前的newFiber添加一个副作用flags 叫新增
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果没有老fiber
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
// newFiber.flags = Placement
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
//将剩下的老fiber放入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
//去map中找找有没key相同并且类型相同可以复用的老fiber 老真实DOM
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
if (newFiber) {
//说明是复用的老fiber
if (newFiber.alternate) {
existingChildren.delete(newFiber.key || newIdx);
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(C)
}
}
//map中剩下是没有被 复用的,全部删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
return resultingFirstChild;
}
reconcileChildrenArray这个方法是DOM DIFF的核心所在,看下运行结果:
到此,DOM DIFF基本结束,其实fiber中有一个调度的过程,根据渲染更新优先级,根据浏览器的响应速度来调度fiber,后面会从这个角度再来看下fiber。