摘要
在上一篇中,实现了多节点的渲染。但是之前写得diff算法,只能适用于单节点的情况,例如这种情况:
javascript
<div>
<p>
<span></span>
</p>
</div>
如果对于多节点的情况:
javascript
<ul>
<li></li>
<li></li>
<li></li>
</ul>
之前实现的diff算法就不会有效果了,所以在这一篇中,我们主要实现针对于多节点的diff算法。
实现之前,我们先将index.js修改一下:
javascript
function App() {
const [num, setNum] = useState(100)
const click1 = () => {
console.log(num);
setNum(num + 1)
}
return num % 2 > 0 ? jsx("ul", {
onClick: click1,
key: 'ul',
children: [jsx("li", {
children: "1",
key: "1"
}), jsx("li", {
children: "2",
key: "2"
}), jsx("li", {
children: "3",
key: "3"
})]
}): jsx("ul", {
onClick: click1,
key: 'ul',
children: [jsx("li", {
children: "2",
key: "2"
}), jsx("li", {
children: "1",
key: "1"
}), jsx("li", {
children: "3",
key: "3"
})]
});
}
ReactDOM.createRoot(root).render(<App />)
1.修改beginWork流程
在reconcileChildren方法里面,我们判断了如果element为数组的情况,就是多节点。所以我们需要在这里进行diff算法的处理。
javascript
function reconcileChildren(parent,element) {
//其他代码。。。。
}else if(Array.isArray(element) && element.length > 0) {
const newChild = diffReconcileManyChildren(parent, element);
if(newChild) {
return newChild
}
//其他代码。。。。
所以我们的diff算法那主要是在diffReconcileManyChildren方法里面实现。
对于多节点的Diff,我们需要进行以下步骤。
- 创建变量lastIndex,用来标记索引
- 将旧的filberNode列表,转换为map结构,key为filberNode的key,value为filberNode
- 遍历新的element数组。
- 如果element.key可以在map中找到,lastIndex记录为找到的filberNode的index
- 如果找不到,创建新的FilberNode
- 继续遍历,如果又在map中找到filberNode,比较fiberNode的index和lastIndex.
- 如果index < lastIndex,给filberNode打上移动的标志
基于上面的步骤,实现diffReconcileManyChildren方法
javascript
function diffReconcileManyChildren(filberNode, element) {
let firstChild = filberNode.child;
if(!firstChild) {
return;
}
const head = {
sibling: null
};
const oldChildren = []
while(firstChild) {
oldChildren.push(firstChild);
firstChild = firstChild.sibling;
}
const oldMap = new Map();
oldChildren.forEach((item,index) => {
item.index = index
if(item.key) {
oldMap.set(item.key, item)
}else{
oldMap.set(index, item)
}
})
let lastIndex = 0;
let empty = head
for(let i=0; i<element.length; i++) {
if(!element[i].key){
continue;
}
const useFilber = oldMap.get(element[i].key);
useFilber.sibling = null;
if(useFilber) {
if(useFilber.index < lastIndex) {
useFilber.flags = 'insert'
}
useFilber.memoizedProps = element[i]
lastIndex = useFilber.index;
empty.sibling = useFilber;
empty = empty.sibling;
oldMap.delete(element[i].key)
}else{
const filberNode = new FilberNode(HostComponent, element[i].props, element[i].key)
filberNode.type = element[i].type
empty.sibling = filberNode;
empty = empty.sibling;
}
}
return head.sibling;
}
经过上面的处理,beginWork流程结束,可复用的filberNode就不会重复创建。
2.修改completeWork流程
在beginWork中,可复用的节点已经被打上了insert的标志,所以在updateCompleteHsotComponent中,我们要判断是不是insert的标志,如果是,就不能无脑创建,而是通过移动DOM的位置来复用DOM。
同时,也要对同级的sibling进行递归处理。
javascript
function updateCompleteHostComponent(filberNode) {
//其他代码。。。。
if(element.key === filberNode.key && element.type === filberNode.type) {
addPropsToDOM(filberNode.stateNode, filberNode.pendingProps);
if(filberNode.flags === 'insert') {
const parent = filberNode.return;
parent.stateNode.insertBefore(filberNode.stateNode, filberNode.sibling?.stateNode)
}
//其他代码
if(filberNode.sibling) {
completeWork(filberNode.sibling)
}
}
在对HostText的处理中,也要考虑,当前的操作是更新还是替换。
javascript
function completeHostText(filberNode) {
//其他代码。。。。。
if(parent && parent.stateNode && parent.tag === HostComponent) {
if(!parent.stateNode) {
parent.stateNode.appendChild(element);
}else{
parent.stateNode.replaceChildren(element);
}
}
//其他代码。。。。
}